Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

900: add field nuernberg pass id & add new pdf template #977

Merged
merged 7 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion administration/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ describe('CreateCardsButtonBar', () => {
id: 0,
name: 'augsburg',
prefix: 'a',
activatedForApplication: true,
}
const cards = [new CardBlueprint('Thea Test', bayernConfig.card, [region])]
const { getByText } = render(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('ImportCardsInput', () => {
id: 0,
name: 'augsburg',
prefix: 'a',
activatedForApplication: true,
}

const renderAndSubmitCardsInput = async (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('useCardGenerator', () => {
id: 0,
name: 'augsburg',
prefix: 'a',
activatedForApplication: true,
}

const cards = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const useCardGenerator = (region: Region) => {
})
: []

const pdfDataUri = await generatePdf(dynamicCodes, staticCodes, region, projectConfig.pdf)
const pdfDataUri = await generatePdf(dynamicCodes, staticCodes, cardBlueprints, region, projectConfig.pdf)

const codes = [...dynamicCodes, ...staticCodes]
await createCards(client, codes, region)
Expand Down
1 change: 1 addition & 0 deletions administration/src/cards/CSVCard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('CSVCard', () => {
id: 0,
name: 'augsburg',
prefix: 'a',
activatedForApplication: true,
}

const cardConfig = {
Expand Down
1 change: 1 addition & 0 deletions administration/src/cards/CardBlueprint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('CardBlueprint', () => {
id: 0,
name: 'augsburg',
prefix: 'a',
activatedForApplication: true,
}

const cardConfig = {
Expand Down
2 changes: 1 addition & 1 deletion administration/src/cards/CardBlueprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class CardBlueprint implements JSONCardBlueprint {
const extensionsMessage: PartialMessage<CardExtensions> = {}

this.extensions.forEach(extension => {
if (extension.state === null) {
if (extension.state === null || !extension.setProtobufData) {
// We allow to skip invalid extensions to enable computing the protobuf size.
return
}
Expand Down
137 changes: 24 additions & 113 deletions administration/src/cards/PdfFactory.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { PDFDocument, PDFFont, PDFPage, StandardFonts } from 'pdf-lib'
import { PDFDocument, PDFPage, StandardFonts } from 'pdf-lib'

import { CardInfo, DynamicActivationCode, QrCode, StaticVerificationCode } from '../generated/card_pb'
import { DynamicActivationCode, QrCode, StaticVerificationCode } from '../generated/card_pb'
import { Region } from '../generated/graphql'
import { PdfConfig } from '../project-configs/getProjectConfig'
import { drawQRCode } from '../util/qrcode'
import CardBlueprint from './CardBlueprint'
import pdfQrCodeElement from './pdf/PdfQrCodeElement'
import pdfTextElement from './pdf/pdfTextElement'

export class PDFError extends Error {
constructor(message: string) {
Expand All @@ -12,135 +14,42 @@ export class PDFError extends Error {
}
}

const dynamicQRCodeSize = 84 // mm
const dynamicQRCodeX = 108 // mm
const dynamicQRCodeY = 73 // mm

const dynamicDetailWidth = 84 // mm
const dynamicDetailX = 108 // mm
const dynamicDetailY = 170 // mm
const dynamicDetailFontSize = 10

const staticBackQRCodeSize = 48 // mm
const staticBackQRCodeX = 51 // mm
const staticBackQRCodeY = 228 // mm

const staticFrontQRCodeSize = 23 // mm
const staticFrontQRCodeX = 156 // mm
const staticFrontQRCodeY = 249 // mm

const staticDetailWidth = 46 // mm
const staticDetailX = 107 // mm
const staticDetailY = 248 // mm
const staticDetailFontSize = 8

function mmToPt(mm: number) {
return (mm / 25.4) * 72
}

type DynamicPdfQrCode = {
value: DynamicActivationCode
case: 'dynamicActivationCode'
}

type StaticPdfQrCode = {
value: StaticVerificationCode
case: 'staticVerificationCode'
}

type PdfQrCode = DynamicPdfQrCode | StaticPdfQrCode

async function fillContentAreas(
doc: PDFDocument,
templatePage: PDFPage,
dynamicCode: DynamicPdfQrCode,
staticCode: StaticPdfQrCode | null,
dynamicCode: Extract<QrCode['qrCode'], { case: 'dynamicActivationCode' }>,
staticCode: Extract<QrCode['qrCode'], { case: 'staticVerificationCode' }> | null,
region: Region,
cardBlueprint: CardBlueprint,
pdfConfig: PdfConfig
) {
const helveticaFont = await doc.embedFont(StandardFonts.Helvetica)

// Dynamic QR code
fillCodeArea(dynamicCode, dynamicQRCodeX, dynamicQRCodeY, dynamicQRCodeSize, templatePage)
fillDetailsArea(
dynamicCode.value.info!,
region,
dynamicDetailX,
dynamicDetailY,
dynamicDetailWidth,
helveticaFont,
dynamicDetailFontSize,
templatePage,
false,
pdfConfig
pdfConfig.elements?.dynamicActivationQrCodes.forEach(configOptions =>
pdfQrCodeElement(configOptions, { page: templatePage, qrCode: dynamicCode })
)

// Static QR code
if (staticCode) {
// Back
fillCodeArea(staticCode, staticBackQRCodeX, staticBackQRCodeY, staticBackQRCodeSize, templatePage)

// Front
fillCodeArea(staticCode, staticFrontQRCodeX, staticFrontQRCodeY, staticFrontQRCodeSize, templatePage)
fillDetailsArea(
staticCode.value.info!,
region,
staticDetailX,
staticDetailY,
staticDetailWidth,
helveticaFont,
staticDetailFontSize,
templatePage,
true,
pdfConfig
pdfConfig.elements?.staticVerificationQrCodes?.forEach(configOptions =>
pdfQrCodeElement(configOptions, { page: templatePage, qrCode: staticCode })
)
}
}

function fillDetailsArea(
info: CardInfo,
region: Region,
x: number,
y: number,
width: number,
font: PDFFont,
fontSize: number,
page: PDFPage,
shorten: boolean,
pdfConfig: PdfConfig
) {
const detailXPdf = mmToPt(x)
const detailYPdf = page.getSize().height - mmToPt(y)

const lineHeight = mmToPt(5)

const text = pdfConfig.infoToDetails(info, region, shorten)
page.drawText(text, {
font,
x: detailXPdf,
y: detailYPdf - lineHeight,
maxWidth: mmToPt(width),
wordBreaks: text.split('').filter(c => !'\n\f\r\u000B'.includes(c)), // Split on every character
lineHeight,
size: fontSize,
})
}

function fillCodeArea(qrCode: PdfQrCode, x: number, y: number, size: number, page: PDFPage) {
const qrCodeSizePdf = mmToPt(size)
const qrCodeXPdf = mmToPt(x)
const qrCodeYPdf = page.getSize().height - qrCodeSizePdf - mmToPt(y)

const qrCodeContent = new QrCode({
qrCode: qrCode,
}).toBinary()

drawQRCode(qrCodeContent, qrCodeXPdf, qrCodeYPdf, qrCodeSizePdf, page, false)
pdfConfig.elements?.text.forEach(configOptions =>
pdfTextElement(configOptions, {
page: templatePage,
font: helveticaFont,
info: dynamicCode.value.info!,
region: region,
cardBlueprint,
})
)
}

export async function generatePdf(
dynamicCodes: DynamicActivationCode[],
staticCodes: StaticVerificationCode[],
cardBlueprints: CardBlueprint[],
region: Region,
pdfConfig: PdfConfig
) {
Expand All @@ -159,6 +68,7 @@ export async function generatePdf(
for (let k = 0; k < dynamicCodes.length; k++) {
const dynamicCode = dynamicCodes[k]
const staticCode = staticCodes?.at(k)
const cardBlueprint = cardBlueprints[k]

const [templatePage] = templateDocument ? await doc.copyPages(templateDocument, [0]) : [null]

Expand All @@ -178,6 +88,7 @@ export async function generatePdf(
}
: null,
region,
cardBlueprint,
pdfConfig
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { FormGroup, InputGroup, Intent } from '@blueprintjs/core'

import { Extension } from './extensions'

const nuernbergPassIdNumberLength = 10

type NuernbergPassIdState = { nuernbergPassId: number }
class NuernbergPassIdExtension extends Extension<NuernbergPassIdState, null> {
public readonly name = NuernbergPassIdExtension.name

setInitialState() {}
createForm(onUpdate: () => void) {
return (
<FormGroup
label='Nürnberg-Pass-ID'
labelFor='nuernberg-pass-id-input'
intent={this.isValid() ? undefined : Intent.DANGER}>
<InputGroup
id='nuernberg-pass-id-input'
placeholder='12345678'
intent={this.isValid() ? undefined : Intent.DANGER}
value={this.state?.nuernbergPassId.toString() ?? ''}
maxLength={nuernbergPassIdNumberLength}
onChange={event => {
const value = event.target.value
if (value.length > nuernbergPassIdNumberLength) {
return
}

const parsedNumber = Number.parseInt(value)

if (isNaN(parsedNumber)) {
this.state = null
onUpdate()
return
}

this.state = {
nuernbergPassId: parsedNumber,
}
onUpdate()
}}
/>
</FormGroup>
)
}

causesInfiniteLifetime() {
return false
}

isValid() {
return (
this.state !== null &&
this.state.nuernbergPassId > 0 &&
this.state.nuernbergPassId < 10 ** nuernbergPassIdNumberLength
)
}

fromString(state: string) {
this.state = { nuernbergPassId: parseInt(state, 10) }
}

toString() {
return this.state ? `${this.state.nuernbergPassId}` : ''
}
}

export default NuernbergPassIdExtension
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ class NuernbergPassNumberExtension extends Extension<NuernbergPassNumberState, n
setInitialState() {}
createForm(onUpdate: () => void) {
return (
<FormGroup label='Passnummer' labelFor='nuernberg-pass-input' intent={this.isValid() ? undefined : Intent.DANGER}>
<FormGroup
label='Nürnberg-Pass-Nummer'
labelFor='nuernberg-pass-number-input'
intent={this.isValid() ? undefined : Intent.DANGER}>
<InputGroup
id='nuernberg-pass-input'
id='nuernberg-pass-number-input'
placeholder='12345678'
intent={this.isValid() ? undefined : Intent.DANGER}
value={this.state?.passNumber.toString() ?? ''}
Expand Down
11 changes: 10 additions & 1 deletion administration/src/cards/extensions/extensions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@ import { ReactElement } from 'react'
import { CardExtensions } from '../../generated/card_pb'
import BavariaCardTypeExtension from './BavariaCardTypeExtension'
import BirthdayExtension from './BirthdayExtension'
import NuernbergPassIdExtension from './NuernbergPassIdExtension'
import NuernbergPassNumberExtension from './NuernbergPassNumberExtension'
import RegionExtension from './RegionExtension'

export const findExtension = <E extends ExtensionClass>(
array: ExtensionInstance[],
extension: E
): InstanceType<E> | undefined => {
return array.find(e => e instanceof extension) as InstanceType<E> | undefined
}

export interface JSONExtension<T> {
name: string
state: T | null
Expand All @@ -19,7 +27,7 @@ export abstract class Extension<T, R> implements JSONExtension<T> {
abstract isValid(): boolean
abstract createForm(onChange: () => void): ReactElement | null
abstract causesInfiniteLifetime(): boolean
abstract setProtobufData(message: PartialMessage<CardExtensions>): void
setProtobufData?(message: PartialMessage<CardExtensions>): void
abstract fromString(state: string): void
abstract toString(): string
}
Expand All @@ -28,5 +36,6 @@ export type ExtensionClass =
| typeof BavariaCardTypeExtension
| typeof BirthdayExtension
| typeof NuernbergPassNumberExtension
| typeof NuernbergPassIdExtension
| typeof RegionExtension
export type ExtensionInstance = InstanceType<ExtensionClass>
13 changes: 13 additions & 0 deletions administration/src/cards/pdf/PdfElements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type Coordinates = {
x: number
y: number
}

export function mmToPt(mm: number) {
return (mm / 25.4) * 72
}

export type PdfElement<ConfigOptions extends Record<string, any>, DynamicOptions extends Record<string, any>> = (
options: ConfigOptions,
dynamicOptions: DynamicOptions
) => void
Loading