Skip to content

feat: report generation enhancements (UI, annotations, PDF formatting)#146

Open
kamil2333 wants to merge 5 commits into
BiometricsUBB:masterfrom
kamil2333:master
Open

feat: report generation enhancements (UI, annotations, PDF formatting)#146
kamil2333 wants to merge 5 commits into
BiometricsUBB:masterfrom
kamil2333:master

Conversation

@kamil2333
Copy link
Copy Markdown

Updates the report generation module with feature selection. It also improves the visual design of the ReportDialog, refines the formatting of the generated PDF and support for hand-drawn annotations.

Features:

  • Allows manual selection and deselection of features to be included in the report directly from the ReportDialog modal.
  • Ensures hand-drawn annotations are correctly rendered on images in the generated report.

Fixes:

  • ReportDialog UI/UX: Improved the overall look and layout of the modal to make it cleaner and more readable.
  • PDF formatting: Applies visual tweaks to improve the final layout of the document.

@kamil2333 kamil2333 changed the title feat: Report generation enhancements (UI, annotations, PDF formatting) feat: report generation enhancements (UI, annotations, PDF formatting) May 6, 2026
[];
const address = (
addressLines.length > 0 ? addressLines : addressFallback
).map(line => stripDiacritics(decodeUnicodeEscapes(line)));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why it was removed?

Comment thread src/lib/locales/en/keywords.ts Outdated
"Include matched only": "Include matched only",
"Matched features": "Matched features",
"Selected features": "Selected features",
"Features list": "Features list",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not used

Comment on lines 565 to 624
@@ -576,8 +620,7 @@ const createStyles = () => {
transform: translateY(-7px);
}
.feature-type { font-size: 9px; }
.feature-image { width: ${IMAGE_CELL_SIZE}px; height: ${IMAGE_CELL_SIZE}px; object-fit: cover; border: 1px solid #ddd; }
.footer { margin-top: auto; font-size: 10px; display: flex; justify-content: space-between; }
.feature-image { width: ${IMAGE_CELL_SIZE}px; height: ${IMAGE_CELL_SIZE}px; object-fit: cover; border: 1px solid #ddd; display: block; }
`;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this to a standalone css file, and import its content via ?raw import

Copy link
Copy Markdown
Collaborator

@TheMultii TheMultii May 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{imported}
.feature-image { width: ${IMAGE_CELL_SIZE}px; }

Comment on lines +157 to +371
<DialogOverlay className="bg-black/40 backdrop-blur-sm z-50" />
<DialogContent className="z-50 w-full sm:w-[880px] max-w-[100vw] sm:max-w-[95vw] max-h-[100dvh] sm:max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden bg-background sm:border-border sm:shadow-2xl rounded-none sm:rounded-xl">

<label
htmlFor="include-matched-only"
className="flex items-center gap-2 text-sm"
>
<input
id="include-matched-only"
type="checkbox"
checked={includeMatchedOnly}
onChange={e =>
setIncludeMatchedOnly(e.target.checked)
}
/>
{t("Include matched only", { ns: "keywords" })}
</label>
<div className="flex flex-col gap-1.5 p-4 sm:p-6 pb-4 border-b border-border bg-muted/10 shrink-0">
<DialogTitle className="text-xl font-semibold tracking-tight text-foreground pr-8">
{t("Report generation", { ns: "keywords" })}
</DialogTitle>
<DialogDescription className="text-sm text-muted-foreground">
{t("Generate PDF report", { ns: "description" })}
</DialogDescription>
</div>

<div className="grid grid-cols-1 gap-3">
<div className="flex flex-col gap-1.5">
<label
htmlFor="report-language"
className="text-sm font-medium"
>
<div className="flex-1 overflow-y-auto md:overflow-hidden p-4 sm:p-6 grid grid-cols-1 md:grid-cols-[1.1fr_0.9fr] gap-6 md:gap-8 bg-background">
<div className="flex flex-col space-y-5 pb-2">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="flex flex-col justify-end space-y-1.5">
<label className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
{t("Language", { ns: "keywords" })}
</label>
<select
id="report-language"
className="h-10 rounded-md border border-input bg-background px-3 text-sm"
className="flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm hover:border-primary/50 focus:ring-2 focus:ring-primary focus:outline-none transition-all cursor-pointer"
value={reportLanguage}
onChange={e =>
setReportLanguage(e.target.value)
}
onChange={e => setReportLanguage(e.target.value)}
>
<option value="pl">Polski</option>
<option value="en">English</option>
</select>
</div>
<div className="flex flex-col gap-1.5">
<label
htmlFor="report-datetime"
className="text-sm font-medium"
>
{t("Report date and time", {
ns: "keywords",
})}
</label>
<div className="flex gap-2">
<Input
id="report-datetime"
value={reportDateTime}
readOnly
placeholder="30.12.2025 - 15:28:31"
/>
</div>
</div>

<div className="flex flex-col gap-1.5">
<label
htmlFor="report-performed-by"
className="text-sm font-medium"
>
{t("Performed by", { ns: "keywords" })}
<div className="flex flex-col justify-end space-y-1.5">
<label className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
{t("Report date and time", { ns: "keywords" })}
</label>
<Input
id="report-performed-by"
value={performedBy}
onChange={e =>
setPerformedBy(e.target.value)
}
placeholder="Jan Kowalski"
value={reportDateTime}
readOnly
className="flex h-10 w-full rounded-md border border-input/60 bg-muted/40 cursor-not-allowed text-sm shadow-sm text-muted-foreground"
/>
</div>
</div>

<div className="flex flex-col gap-1.5">
<label
htmlFor="report-department"
className="text-sm font-medium"
>
{t("Department", { ns: "keywords" })}
<div className="flex flex-col space-y-1.5">
<label className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
{t("Performed by", { ns: "keywords" })}
</label>
<Input
value={performedBy}
onChange={e => setPerformedBy(e.target.value)}
className="flex h-10 w-full rounded-md border border-input bg-background shadow-sm hover:border-primary/50 transition-colors"
/>
</div>

<div className="flex flex-col space-y-1.5">
<label className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
{t("Department", { ns: "keywords" })}
</label>
<Input
value={department}
onChange={e => setDepartment(e.target.value)}
className="flex h-10 w-full rounded-md border border-input bg-background shadow-sm hover:border-primary/50 transition-colors"
/>
</div>

<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-1">
<div className="flex flex-col space-y-1.5">
<label className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
{t("Address line 1", { ns: "keywords" })}
</label>
<Input
id="report-department"
value={department}
onChange={e =>
setDepartment(e.target.value)
}
placeholder="Wydzia\u0142 Bada\u0144 Daktyloskopijnych i Traseologicznych"
<Input
value={addressLine1}
onChange={e => setAddressLine1(e.target.value)}
className="flex h-10 w-full rounded-md border border-input bg-background shadow-sm hover:border-primary/50 transition-colors"
/>
</div>

<div className="flex flex-col gap-1.5">
<label
htmlFor="report-address-1"
className="text-sm font-medium"
>
{t("Address line 1", {
ns: "keywords",
})}
<div className="flex flex-col space-y-1.5">
<label className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
{t("Address line 2", { ns: "keywords" })}
</label>
<Input
id="report-address-1"
value={addressLine1}
onChange={e =>
setAddressLine1(e.target.value)
}
placeholder="ul. Mi\u0142a 1"
<Input
value={addressLine2}
onChange={e => setAddressLine2(e.target.value)}
className="flex h-10 w-full rounded-md border border-input bg-background shadow-sm hover:border-primary/50 transition-colors"
/>
</div>

<div className="flex flex-col gap-1.5">
<label
htmlFor="report-address-2"
className="text-sm font-medium"
>
{t("Address line 2", {
ns: "keywords",
})}
<div className="flex flex-col space-y-1.5">
<label className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
{t("Address line 3", { ns: "keywords" })}
</label>
<Input
id="report-address-2"
value={addressLine2}
onChange={e =>
setAddressLine2(e.target.value)
}
placeholder="02-520 Warszawa"
<Input
value={addressLine3}
onChange={e => setAddressLine3(e.target.value)}
className="flex h-10 w-full rounded-md border border-input bg-background shadow-sm hover:border-primary/50 transition-colors"
/>
</div>

<div className="flex flex-col gap-1.5">
<label
htmlFor="report-address-3"
className="text-sm font-medium"
>
{t("Address line 3", {
ns: "keywords",
})}
<div className="flex flex-col space-y-1.5">
<label className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
{t("Address line 4", { ns: "keywords" })}
</label>
<Input
id="report-address-3"
value={addressLine3}
onChange={e =>
setAddressLine3(e.target.value)
}
placeholder=""
<Input
value={addressLine4}
onChange={e => setAddressLine4(e.target.value)}
className="flex h-10 w-full rounded-md border border-input bg-background shadow-sm hover:border-primary/50 transition-colors"
/>
</div>
</div>
</div>

<div className="flex flex-col gap-1.5">
<label
htmlFor="report-address-4"
className="text-sm font-medium"
>
{t("Address line 4", {
ns: "keywords",
})}
</label>
<Input
id="report-address-4"
value={addressLine4}
onChange={e =>
setAddressLine4(e.target.value)
}
placeholder=""
<div className="flex flex-col space-y-4 md:min-h-0 md:h-full">
<div className="bg-card p-4 rounded-lg border border-border flex justify-between items-center shadow-sm shrink-0">
<div className="flex flex-col">
<span className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
{t("Matched features", { ns: "keywords" })}
</span>
<span className="text-2xl font-bold text-foreground">
{matchedFeaturesCount}
</span>
</div>
<div className="w-px h-10 bg-border hidden xs:block"></div>
<div className="flex flex-col items-end">
<span className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
{t("Selected features", { ns: "keywords" })}
</span>
<span className="text-2xl font-bold text-primary">
{selectedLabels.length} <span className="text-muted-foreground text-lg">/ {allFeatures.length}</span>
</span>
</div>
</div>

<label className="flex items-center gap-2.5 cursor-pointer text-sm font-medium w-max hover:text-primary transition-colors shrink-0">
<div className="relative flex items-center justify-center">
<input
type="checkbox"
className="peer appearance-none w-4 h-4 border border-input rounded-sm bg-background checked:bg-primary checked:border-primary transition-all cursor-pointer shadow-sm"
checked={includeMatchedOnly}
onChange={e => setIncludeMatchedOnly(e.target.checked)}
/>
<Check className="absolute w-3 h-3 text-primary-foreground opacity-0 peer-checked:opacity-100 pointer-events-none" strokeWidth={3} />
</div>
{t("Include matched only", { ns: "keywords" })}
</label>

<div className="md:flex-1 md:min-h-0 rounded-lg border border-border bg-muted/5 p-2 md:overflow-y-auto custom-scrollbar shadow-inner">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{allFeatures.map((label) => {
const left = markingsLeft.find(m => m.label === label);
const right = markingsRight.find(m => m.label === label);
const isSelected = selectedLabels.includes(label);

return (
<div
key={label}
onClick={() => toggleFeature(label)}
className={cn(
"flex flex-col gap-2 p-3 rounded-md border cursor-pointer transition-all duration-200 select-none bg-background",
isSelected
? "border-primary ring-1 ring-primary/20 shadow-sm"
: "border-border hover:border-primary/50 hover:bg-muted/30 opacity-80"
)}
>
<div className="flex items-center gap-2.5">
<div className={cn(
"w-4 h-4 rounded-sm border flex items-center justify-center shrink-0 transition-colors shadow-sm",
isSelected ? "bg-primary border-primary text-primary-foreground" : "bg-card border-input"
)}>
{isSelected && <Check size={12} strokeWidth={3} />}
</div>
<span className={cn(
"text-sm font-semibold truncate",
isSelected ? "text-foreground" : "text-muted-foreground"
)}>
{t("Feature", { ns: "keywords" })} #{label}
</span>
</div>
<div className="flex justify-between pl-6.5 text-xs font-medium">
<span className={cn("px-1.5 py-0.5 rounded-sm border", left ? "bg-emerald-500/10 border-emerald-500/20 text-emerald-600 dark:text-emerald-400" : "bg-muted/50 border-border/50 text-muted-foreground")}>
L: {left ? "OK" : "—"}
</span>
<span className={cn("px-1.5 py-0.5 rounded-sm border", right ? "bg-emerald-500/10 border-emerald-500/20 text-emerald-600 dark:text-emerald-400" : "bg-muted/50 border-border/50 text-muted-foreground")}>
R: {right ? "OK" : "—"}
</span>
</div>
</div>
);
})}
</div>
</div>
</div>
</div>

<div className="mt-6 flex justify-between shrink-0">
<div className="p-4 sm:px-6 border-t border-border bg-muted/10 flex flex-col-reverse sm:flex-row justify-end gap-3 shrink-0">
<DialogClose asChild>
<Button type="button" variant="outline">
<Button type="button" variant="outline" className="w-full sm:w-28 shadow-sm">
{t("Cancel", { ns: "keywords" })}
</Button>
</DialogClose>
<Button
type="button"
onClick={onGenerate}
disabled={!canGenerate || isGenerating}
className="w-full sm:w-44 shadow-sm"
disabled={!canGenerate || isGenerating || selectedLabels.length === 0}
>
{isGenerating
? t("Generating...", { ns: "keywords" })
: generateReportLabel}
</Button>
</div>

<DialogClose className="absolute top-3 right-3">
<X size={ICON.SIZE} strokeWidth={ICON.STROKE_WIDTH} />
<DialogClose className="absolute top-4 right-4 sm:top-6 sm:right-6 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none text-muted-foreground hover:bg-muted p-1">
<X size={20} strokeWidth={ICON.STROKE_WIDTH} />
</DialogClose>
</DialogContent>
</DialogPortal>
</Dialog>
);
}
} No newline at end of file
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a lot of repetition that screams for extraction

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants