Generate adoption reports as PDFs
Deploy / deploy-dev (push) Successful in 2m24s
Deploy / deploy-prod (push) Has been skipped

This commit is contained in:
Corey Blais
2026-06-02 17:49:31 -04:00
parent c2d518f864
commit 7e2d06c50b
6 changed files with 985 additions and 4 deletions
+89
View File
@@ -64,6 +64,7 @@ import {
updateVetVisitForBird,
} from './repositories/birdRepository.js';
import { createIntegrationTokenRecord, listIntegrationTokens, revokeIntegrationToken } from './repositories/integrationTokenRepository.js';
import { renderAdoptionReportPdf } from './reports/adoptionReport.js';
import {
createAuditLogEntry,
createFlockNote,
@@ -163,6 +164,7 @@ const trustProxy = process.env.TRUST_PROXY?.trim() ?? '';
const milestoneRemindersEnabled = (process.env.MILESTONE_REMINDERS_ENABLED ?? 'true').toLowerCase() !== 'false';
const milestoneReminderTimeZone = process.env.MILESTONE_REMINDER_TIME_ZONE?.trim() || 'America/New_York';
const milestoneReminderCheckIntervalMs = 60 * 60 * 1000;
const adoptionReportWeightHistoryDays = 425;
const photoDeliveryMode = process.env.PHOTO_DELIVERY_MODE === 'redirect' ? 'redirect' : 'proxy';
if (trustProxy) {
@@ -835,6 +837,7 @@ app.disable('x-powered-by');
app.use(helmet({ crossOriginResourcePolicy: false }));
app.use(
cors({
exposedHeaders: ['X-FlockPal-Transfer-Code'],
origin(origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
@@ -3522,6 +3525,92 @@ app.post(
},
);
app.post(
'/api/birds/:birdId/reports/adoption',
requireAuth,
requireWriteAccess,
requireSessionAuth,
requireWorkspaceRole(['owner', 'assistant']),
async (req: Request, res: Response, next: NextFunction) => {
try {
const sourceBird = await getBirdById(req.params.birdId, req.auth!.workspace.id);
if (!sourceBird) {
res.status(404).json({ error: 'Bird not found.' });
return;
}
if (!ensureBirdWritable(sourceBird, res)) {
return;
}
let transferCode = null;
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
transferCode = await createBirdTransferCode({
code: createBirdTransferCodeValue(),
birdId: sourceBird.id,
sourceWorkspaceId: req.auth!.workspace.id,
requestedByUserId: req.auth!.user.id,
});
break;
} catch (error) {
if (typeof error === 'object' && error && 'code' in error && error.code === '23505' && attempt < 2) {
continue;
}
throw error;
}
}
if (!transferCode) {
throw new Error('Unable to create bird transfer code.');
}
const [weights, vetVisits, notes] = await Promise.all([
listWeightsForBird(sourceBird.id, req.auth!.workspace.id, adoptionReportWeightHistoryDays),
listVetVisitsForBird(sourceBird.id, req.auth!.workspace.id),
listFlockNotes(req.auth!.workspace.id),
]);
const birdNotes = notes.filter((note) => note.bird_id === sourceBird.id);
const pdf = await renderAdoptionReportPdf({
bird: sourceBird,
weights,
vetVisits,
notes: birdNotes,
workspace: req.auth!.workspace,
transferCode: transferCode.code,
printFriendly: req.query.printFriendly === 'true',
assets: {
logoPath: path.join(process.cwd(), 'assets', 'flockpal-logo.png'),
wordmarkPath: path.join(process.cwd(), 'assets', 'flockpal-text.png'),
defaultBirdPhotoPath: path.join(process.cwd(), 'assets', 'yoda-default.png'),
},
});
await writeAuditLog(req.auth!, 'bird.adoption_report_created', 'bird', sourceBird.id, sourceBird.name, {
transferCodeId: transferCode.id,
printFriendly: req.query.printFriendly === 'true',
});
const safeName = sourceBird.name
.trim()
.replace(/[^a-z0-9]+/gi, '-')
.replace(/^-+|-+$/g, '')
.toLowerCase() || 'bird';
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="flockpal-adoption-report-${safeName}.pdf"`);
res.setHeader('Content-Length', pdf.length.toString());
res.setHeader('X-FlockPal-Transfer-Code', transferCode.code);
res.send(pdf);
} catch (error) {
next(error);
}
},
);
app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceRole(['owner', 'assistant', 'caregiver']), async (req: Request, res: Response, next: NextFunction) => {
const parsed = birdSchema.safeParse(req.body);