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.
90 lines
2.6 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|
|
}
|