Files
ApiCore/ApiCore-Backend/src/qr/qr.service.ts
T
Nick Szaunig 53b781aa04 feat: NFC-Tags beschreiben mit demselben Deep-Link wie der QR-Code
Neuer Endpunkt GET /api/v1/qr/:entityType/:id/link liefert den vom
QR-Dienst bereits genutzten Deep-Link als reine URL statt als SVG,
damit die Web-NFC-API (client-seitig, Chrome/Android) ihn per
NDEFReader.write() auf einen physischen NFC-Tag schreiben kann.
QrService.resolveUrl() ist aus generateSvg() herausgezogen, damit
beide Endpunkte exakt denselben Link erzeugen.

Der bestehende QrCodeDialog bekommt dadurch für alle sechs
QR-fähigen Entitäten (Völker, Bienenstände, Königinnen, Honigchargen,
Lagerbehälter, Abfüllungen) automatisch einen "Auf NFC-Tag
schreiben"-Button, der nur erscheint, wenn window.NDEFReader
tatsächlich existiert – kein Fallback für andere Browser oder iOS,
wie in der Lastenheft vorgesehen.
2026-08-25 01:50:14 +02:00

90 lines
2.6 KiB
TypeScript

import {
Injectable,
BadRequestException,
NotFoundException,
} from '@nestjs/common';
import * as QRCode from 'qrcode';
import { PrismaService } from '../prisma/prisma.service';
const ENTITY_TYPES = [
'colonies',
'apiaries',
'queens',
'honey-batches',
'storage-containers',
'bottling-batches',
] as const;
type EntityType = (typeof ENTITY_TYPES)[number];
function isEntityType(value: string): value is EntityType {
return (ENTITY_TYPES as readonly string[]).includes(value);
}
@Injectable()
export class QrService {
constructor(private readonly prisma: PrismaService) {}
async generateSvg(entityType: string, id: string): Promise<string> {
const url = await this.resolveUrl(entityType, id);
return QRCode.toString(url, { type: 'svg', margin: 1, width: 256 });
}
async resolveUrl(entityType: string, id: string): Promise<string> {
if (!isEntityType(entityType)) {
throw new BadRequestException(
`Unsupported QR entity type: ${entityType}`,
);
}
const deepLink = await this.resolveDeepLink(entityType, id);
if (!deepLink) {
throw new NotFoundException('Object not found');
}
const frontendOrigin =
process.env.FRONTEND_ORIGIN ?? 'http://localhost:3000';
return `${frontendOrigin}${deepLink}`;
}
private async resolveDeepLink(
entityType: EntityType,
id: string,
): Promise<string | null> {
switch (entityType) {
case 'colonies': {
const colony = await this.prisma.colony.findUnique({
where: { id },
});
return colony ? `/inspections/new?colonyId=${id}` : null;
}
case 'apiaries': {
const apiary = await this.prisma.apiary.findUnique({ where: { id } });
return apiary ? `/apiaries/${id}` : null;
}
case 'queens': {
const queen = await this.prisma.queen.findUnique({ where: { id } });
return queen ? `/queens/${id}` : null;
}
case 'honey-batches': {
const honeyBatch = await this.prisma.honeyBatch.findUnique({
where: { id },
});
return honeyBatch ? `/honey-batches/${id}` : null;
}
case 'storage-containers': {
const container = await this.prisma.storageContainer.findUnique({
where: { id },
});
return container ? `/storage` : null;
}
case 'bottling-batches': {
const bottlingBatch = await this.prisma.bottlingBatch.findUnique({
where: { id },
select: { lotNumber: true },
});
return bottlingBatch ? `/herkunft/${bottlingBatch.lotNumber}` : null;
}
}
}
}