feat(registratura): smart subject autocomplete v2 — seed templates, project linking, dynamic placeholders

- Add SEED_TEMPLATES catalog (11 doc types x 2-4 templates = ~30 predefined patterns)
- Add {proiect} field type with mini-autocomplete from Tag Manager projects
- Pre-fill {an} fields with current year on template selection
- Dynamic placeholder changes based on documentType + direction (22 combinations)
- Dropdown on empty focus: "Sabloane recomandate" + "Recente" sections
- Direction-aware tooltip on Subiect field (intrat vs iesit examples)
- getRecommendedTemplates() merges seeds + DB-learned, DB takes priority

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-10 12:46:05 +02:00
parent 3a3db3f366
commit e30b437dce
4 changed files with 551 additions and 55 deletions
@@ -25,6 +25,8 @@ import {
assembleSubject,
filterTemplates,
filterSubjects,
getRecommendedTemplates,
getSubjectPlaceholder,
} from "../services/subject-template-service";
import type { SubjectTemplate } from "../services/subject-template-service";
import { SubjectTemplateInput } from "./subject-template-input";
@@ -113,6 +115,7 @@ export function RegistryEntryForm({
}: RegistryEntryFormProps) {
const { allContacts, refresh: refreshContacts } = useContacts();
const { tags: docTypeTags } = useTags("document-type");
const { tags: projectTags } = useTags("project");
const fileInputRef = useRef<HTMLInputElement>(null);
// Track locally-added custom types that may not yet be in Tag Manager
@@ -336,6 +339,37 @@ export function RegistryEntryForm({
[allSubjects, subjectQuery],
);
// Recommended templates for current doc type (seeds + DB-learned)
const recommendedTemplates = useMemo(
() => getRecommendedTemplates(documentType, allTemplates),
[documentType, allTemplates],
);
// Recent subjects from DB (last 5 unique, sorted by newest first)
const recentSubjects = useMemo(() => {
if (!allEntries || allEntries.length === 0) return [];
const sorted = [...allEntries]
.filter((e) => e.subject && !e.isReserved)
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
const seen = new Set<string>();
const result: string[] = [];
for (const e of sorted) {
if (result.length >= 5) break;
const lower = e.subject.toLowerCase();
if (!seen.has(lower)) {
seen.add(lower);
result.push(e.subject);
}
}
return result;
}, [allEntries]);
// Dynamic placeholder based on document type + direction
const subjectPlaceholder = useMemo(
() => getSubjectPlaceholder(documentType, direction),
[documentType, direction],
);
// ── Quick contact creation handler ──
const openQuickContact = (
field: "sender" | "recipient" | "assignee",
@@ -717,11 +751,29 @@ export function RegistryEntryForm({
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p className="text-xs">
Scurt și descriptiv: «Cerere CU nr. 123/2026 Str.
Exemplu» sau «Aviz ISU Proiect X». Nu folosi prefixe de
numerotare.
</p>
{direction === "intrat" ? (
<div className="text-xs space-y-1">
<p className="font-medium">Documente primite:</p>
<ul className="list-disc pl-3 space-y-0.5">
<li>Cerere CU nr. 123/2026 Proiect X</li>
<li>Aviz ISU nr. 55/2026 Proiect X</li>
<li>Contract nr. 15/2026 Proiect X</li>
<li>Factura nr. 102/2026 Proiect X</li>
</ul>
<p className="text-muted-foreground">Selecteaza un sablon sau tasteaza liber.</p>
</div>
) : (
<div className="text-xs space-y-1">
<p className="font-medium">Documente trimise:</p>
<ul className="list-disc pl-3 space-y-0.5">
<li>Oferta proiectare DTAC Proiect X</li>
<li>Solicitare aviz DSP Proiect X</li>
<li>Notificare incepere lucrari Proiect X</li>
<li>Raport expertiza tehnica Proiect X</li>
</ul>
<p className="text-muted-foreground">Selecteaza un sablon sau tasteaza liber.</p>
</div>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -740,6 +792,7 @@ export function RegistryEntryForm({
setActiveTemplate(null);
setSubjectQuery(subject);
}}
projectTags={projectTags}
/>
) : (
<Input
@@ -752,23 +805,30 @@ export function RegistryEntryForm({
onBlur={() => setTimeout(() => setSubjectFocused(false), 200)}
className="mt-1"
required
placeholder="Tastează pentru sugestii..."
placeholder={subjectPlaceholder}
/>
)}
<SubjectAutocompleteDropdown
templates={matchingTemplates}
suggestions={matchingSuggestions}
recommendedTemplates={recommendedTemplates}
recentSubjects={recentSubjects}
visible={
subjectFocused &&
!activeTemplate &&
subjectQuery.length >= 2 &&
(matchingTemplates.length > 0 || matchingSuggestions.length > 0)
!activeTemplate
}
onSelectTemplate={(t) => {
// Pre-fill year fields with current year
const prefill: Record<string, string> = {};
for (const field of t.fields) {
if (field.defaultValue) {
prefill[field.id] = field.defaultValue;
}
}
setActiveTemplate(t);
setTemplateFieldValues({});
setSubject(assembleSubject(t, {}));
setTemplateFieldValues(prefill);
setSubject(assembleSubject(t, prefill));
setSubjectFocused(false);
}}
onSelectSuggestion={(s) => {