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.
This commit is contained in:
1 parent
f346859121
commit
53b781aa04
8 files changed
+106
-5
No files matched your search
@@ -19,4 +19,13 @@ export class QrController {
|
||||
res.setHeader('Content-Type', 'image/svg+xml');
|
||||
res.send(svg);
|
||||
}
|
||||
|
||||
@Get(':entityType/:id/link')
|
||||
async link(
|
||||
@Param('entityType') entityType: string,
|
||||
@Param('id') id: string,
|
||||
): Promise<{ url: string }> {
|
||||
const url = await this.qrService.resolveUrl(entityType, id);
|
||||
return { url };
|
||||
}
|
||||
}
|
||||
@@ -57,4 +57,23 @@ describe('QrService', () => {
|
||||
const svg = await service.generateSvg('bottling-batches', 'bb-1');
|
||||
expect(svg).toContain('<svg');
|
||||
});
|
||||
|
||||
it('resolveUrl returns the same deep-link that generateSvg encodes, for NFC writing', async () => {
|
||||
prisma.colony.findUnique.mockResolvedValueOnce({ id: 'colony-1' });
|
||||
const url = await service.resolveUrl('colonies', 'colony-1');
|
||||
expect(url).toBe('http://localhost:3000/inspections/new?colonyId=colony-1');
|
||||
});
|
||||
|
||||
it('resolveUrl rejects an unsupported entity type', async () => {
|
||||
await expect(service.resolveUrl('unknown', 'id-1')).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('resolveUrl throws NotFoundException when the object does not exist', async () => {
|
||||
prisma.colony.findUnique.mockResolvedValueOnce(null);
|
||||
await expect(
|
||||
service.resolveUrl('colonies', 'missing'),
|
||||
).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,11 @@ 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}`,
|
||||
@@ -38,8 +43,7 @@ export class QrService {
|
||||
|
||||
const frontendOrigin =
|
||||
process.env.FRONTEND_ORIGIN ?? 'http://localhost:3000';
|
||||
const url = `${frontendOrigin}${deepLink}`;
|
||||
return QRCode.toString(url, { type: 'svg', margin: 1, width: 256 });
|
||||
return `${frontendOrigin}${deepLink}`;
|
||||
}
|
||||
|
||||
private async resolveDeepLink(
|
||||
|
||||
@@ -78,4 +78,13 @@ describe('QR codes (e2e)', () => {
|
||||
.set('Authorization', `Bearer ${beekeeperToken}`)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('returns the plain deep-link URL for NFC writing', async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v1/qr/colonies/${colonyId}/link`)
|
||||
.set('Authorization', `Bearer ${beekeeperToken}`)
|
||||
.expect(200);
|
||||
const body = response.body as { url: string };
|
||||
expect(body.url).toContain(`colonyId=${colonyId}`);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user