feat: report generation enhancements (UI, annotations, PDF formatting)#146
Open
kamil2333 wants to merge 5 commits into
Open
feat: report generation enhancements (UI, annotations, PDF formatting)#146kamil2333 wants to merge 5 commits into
kamil2333 wants to merge 5 commits into
Conversation
TheMultii
requested changes
May 22, 2026
| []; | ||
| const address = ( | ||
| addressLines.length > 0 ? addressLines : addressFallback | ||
| ).map(line => stripDiacritics(decodeUnicodeEscapes(line))); |
| "Include matched only": "Include matched only", | ||
| "Matched features": "Matched features", | ||
| "Selected features": "Selected features", | ||
| "Features list": "Features list", |
TheMultii
requested changes
May 26, 2026
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; } | |||
| `; | |||
Collaborator
There was a problem hiding this comment.
move this to a standalone css file, and import its content via ?raw import
Collaborator
There was a problem hiding this comment.
{imported}
.feature-image { width: ${IMAGE_CELL_SIZE}px; }
TheMultii
requested changes
May 26, 2026
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 |
Collaborator
There was a problem hiding this comment.
there's a lot of repetition that screams for extraction
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
ReportDialogmodal.Fixes: