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:
@@ -2,16 +2,25 @@
|
||||
* Subject Template Service — extracts reusable templates from existing
|
||||
* registry subjects and provides filtering/assembly utilities.
|
||||
*
|
||||
* Also provides seed templates per document type so the autocomplete
|
||||
* is useful from the very first entry.
|
||||
*
|
||||
* Pure functions, no React or side effects.
|
||||
*/
|
||||
|
||||
import type { RegistryDirection } from "../types";
|
||||
|
||||
// ── Types ──
|
||||
|
||||
export type TemplateFieldType = "nr" | "an" | "detalii" | "proiect" | "text";
|
||||
|
||||
export interface TemplateField {
|
||||
id: string; // "f0", "f1", ...
|
||||
name: string; // "nr", "an", "detalii"
|
||||
name: string; // "nr", "an", "detalii", "proiect"
|
||||
placeholder: string; // Romanian label
|
||||
width: string; // Tailwind width class
|
||||
fieldType?: TemplateFieldType;
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
export interface TemplateToken {
|
||||
@@ -22,11 +31,13 @@ export interface TemplateToken {
|
||||
|
||||
export interface SubjectTemplate {
|
||||
id: string;
|
||||
pattern: string; // "Cerere CU nr. {nr}/{an} — {detalii}"
|
||||
pattern: string; // "Cerere CU nr. {nr}/{an} — {proiect}"
|
||||
tokens: TemplateToken[];
|
||||
fields: TemplateField[];
|
||||
frequency: number;
|
||||
exampleSubject: string;
|
||||
/** Whether this is a predefined seed template (not learned from DB) */
|
||||
isSeed?: boolean;
|
||||
}
|
||||
|
||||
// ── Internal helpers ──
|
||||
@@ -75,6 +86,7 @@ function tokenize(subject: string): TemplateToken[] {
|
||||
name: "detalii",
|
||||
placeholder: "detalii...",
|
||||
width: "flex-1 min-w-32",
|
||||
fieldType: "detalii",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -109,6 +121,7 @@ function tokenizeSegment(
|
||||
}
|
||||
|
||||
const isYear = match[1] !== undefined;
|
||||
const ft: TemplateFieldType = isYear ? "an" : "nr";
|
||||
tokens.push({
|
||||
type: "field",
|
||||
value: "",
|
||||
@@ -117,6 +130,7 @@ function tokenizeSegment(
|
||||
name: isYear ? "an" : "nr",
|
||||
placeholder: isYear ? "an" : "nr.",
|
||||
width: isYear ? "w-16" : "w-14",
|
||||
fieldType: ft,
|
||||
},
|
||||
});
|
||||
idx++;
|
||||
@@ -146,8 +160,242 @@ function buildPattern(tokens: TemplateToken[]): string {
|
||||
.join("");
|
||||
}
|
||||
|
||||
// ── Seed template builder ──
|
||||
|
||||
const CURRENT_YEAR = new Date().getFullYear().toString();
|
||||
|
||||
/**
|
||||
* Build a seed SubjectTemplate from a pattern string.
|
||||
* Parses `{fieldName}` placeholders into tokens/fields.
|
||||
*/
|
||||
function seedTemplate(pattern: string, example: string): SubjectTemplate {
|
||||
const tokens: TemplateToken[] = [];
|
||||
const fields: TemplateField[] = [];
|
||||
let fIdx = 0;
|
||||
|
||||
// Split pattern by {fieldName} placeholders
|
||||
const parts = pattern.split(/(\{[^}]+\})/);
|
||||
for (const part of parts) {
|
||||
if (!part) continue;
|
||||
const fieldMatch = part.match(/^\{(\w+)\}$/);
|
||||
if (fieldMatch) {
|
||||
const name = fieldMatch[1]!;
|
||||
const ft = resolveFieldType(name);
|
||||
const field: TemplateField = {
|
||||
id: `f${fIdx}`,
|
||||
name,
|
||||
placeholder: fieldPlaceholder(ft),
|
||||
width: fieldWidth(ft),
|
||||
fieldType: ft,
|
||||
defaultValue: ft === "an" ? CURRENT_YEAR : undefined,
|
||||
};
|
||||
fields.push(field);
|
||||
tokens.push({ type: "field", value: "", field });
|
||||
fIdx++;
|
||||
} else {
|
||||
tokens.push({ type: "static", value: part });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: hashPattern(pattern),
|
||||
pattern,
|
||||
tokens,
|
||||
fields,
|
||||
frequency: 0,
|
||||
exampleSubject: example,
|
||||
isSeed: true,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveFieldType(name: string): TemplateFieldType {
|
||||
if (name === "nr") return "nr";
|
||||
if (name === "an") return "an";
|
||||
if (name === "proiect") return "proiect";
|
||||
if (name === "detalii") return "detalii";
|
||||
return "text";
|
||||
}
|
||||
|
||||
function fieldPlaceholder(ft: TemplateFieldType): string {
|
||||
switch (ft) {
|
||||
case "nr": return "nr.";
|
||||
case "an": return "an";
|
||||
case "proiect": return "proiect...";
|
||||
case "detalii": return "detalii...";
|
||||
case "text": return "text...";
|
||||
}
|
||||
}
|
||||
|
||||
function fieldWidth(ft: TemplateFieldType): string {
|
||||
switch (ft) {
|
||||
case "nr": return "w-14";
|
||||
case "an": return "w-16";
|
||||
case "proiect": return "flex-1 min-w-32";
|
||||
case "detalii": return "flex-1 min-w-32";
|
||||
case "text": return "flex-1 min-w-24";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Seed Templates Catalog ──
|
||||
|
||||
const SEED_TEMPLATES: Record<string, SubjectTemplate[]> = {
|
||||
cerere: [
|
||||
seedTemplate("Cerere CU nr. {nr}/{an} — {proiect}", "Cerere CU nr. 123/2026 — Farmacie Str. Exemplu"),
|
||||
seedTemplate("Cerere AC nr. {nr}/{an} — {proiect}", "Cerere AC nr. 456/2026 — Locuinta P+1E"),
|
||||
seedTemplate("Cerere prelungire CU nr. {nr}/{an} — {proiect}", "Cerere prelungire CU nr. 78/2026 — Bloc D4"),
|
||||
seedTemplate("Cerere prelungire AC nr. {nr}/{an} — {proiect}", "Cerere prelungire AC nr. 90/2026 — Hala industriala"),
|
||||
],
|
||||
aviz: [
|
||||
seedTemplate("Aviz {detalii} nr. {nr}/{an} — {proiect}", "Aviz ISU nr. 55/2026 — Cladire birouri"),
|
||||
seedTemplate("Solicitare aviz {detalii} — {proiect}", "Solicitare aviz DSP — Gradinita nr. 3"),
|
||||
seedTemplate("Aviz de oportunitate nr. {nr}/{an} — {proiect}", "Aviz de oportunitate nr. 12/2026 — PUZ Zona centrala"),
|
||||
],
|
||||
contract: [
|
||||
seedTemplate("Contract nr. {nr}/{an} — {proiect}", "Contract nr. 15/2026 — Proiectare locuinta"),
|
||||
seedTemplate("Contract prestari servicii nr. {nr}/{an} — {proiect}", "Contract prestari servicii nr. 22/2026 — Studiu geo"),
|
||||
seedTemplate("Act aditional nr. {nr} la contract {detalii}", "Act aditional nr. 2 la contract 15/2026"),
|
||||
],
|
||||
oferta: [
|
||||
seedTemplate("Oferta proiectare {detalii} — {proiect}", "Oferta proiectare DTAC+PT — Bloc rezidential"),
|
||||
seedTemplate("Oferta nr. {nr}/{an} — {proiect}", "Oferta nr. 8/2026 — Mansardare bloc"),
|
||||
],
|
||||
factura: [
|
||||
seedTemplate("Factura nr. {nr}/{an} — {proiect}", "Factura nr. 102/2026 — Farmacie Str. Exemplu"),
|
||||
seedTemplate("Factura proforma nr. {nr}/{an} — {proiect}", "Factura proforma nr. 5/2026 — Studiu geo"),
|
||||
],
|
||||
scrisoare: [
|
||||
seedTemplate("Adresa nr. {nr}/{an} — {detalii}", "Adresa nr. 45/2026 — Raspuns solicitare informatii"),
|
||||
seedTemplate("Notificare {detalii} — {proiect}", "Notificare incepere lucrari — Locuinta P+1E"),
|
||||
seedTemplate("Adeverinta {detalii} nr. {nr}/{an}", "Adeverinta stagiu practica nr. 3/2026"),
|
||||
],
|
||||
"nota-de-comanda": [
|
||||
seedTemplate("Nota de comanda nr. {nr}/{an} — {proiect}", "Nota de comanda nr. 7/2026 — Instalatii electrice"),
|
||||
seedTemplate("Comanda {detalii} — {proiect}", "Comanda materiale — Santier Bloc A"),
|
||||
],
|
||||
raport: [
|
||||
seedTemplate("Raport {detalii} — {proiect}", "Raport expertiza tehnica — Consolidare bloc"),
|
||||
seedTemplate("Studiu geotehnic {detalii} — {proiect}", "Studiu geotehnic preliminar — Hala logistica"),
|
||||
seedTemplate("Raport nr. {nr}/{an} — {proiect}", "Raport nr. 3/2026 — Audit energetic"),
|
||||
],
|
||||
"apel-telefonic": [
|
||||
seedTemplate("Convorbire {detalii} — {proiect}", "Convorbire primarie avize — Bloc D4"),
|
||||
seedTemplate("Apel {detalii} — {proiect}", "Apel ISU programare — Gradinita nr. 3"),
|
||||
],
|
||||
videoconferinta: [
|
||||
seedTemplate("Sedinta {detalii} — {proiect}", "Sedinta coordonare proiectare — Bloc rezidential"),
|
||||
seedTemplate("Videoconferinta {detalii} — {proiect}", "Videoconferinta revizie PT — Hala industriala"),
|
||||
],
|
||||
altele: [
|
||||
seedTemplate("Documentatie {detalii} — {proiect}", "Documentatie PAC+DDE — Locuinta P+M"),
|
||||
seedTemplate("{detalii} nr. {nr}/{an} — {proiect}", "PV receptie nr. 5/2026 — Cladire birouri"),
|
||||
],
|
||||
};
|
||||
|
||||
// ── Dynamic Placeholders ──
|
||||
|
||||
const SUBJECT_PLACEHOLDERS_MAP: Record<string, Record<string, string>> = {
|
||||
cerere: {
|
||||
intrat: "ex: Cerere CU nr. 123/2026 — Proiect X",
|
||||
iesit: "ex: Cerere prelungire AC nr. 45/2026 — Proiect X",
|
||||
},
|
||||
aviz: {
|
||||
intrat: "ex: Aviz ISU nr. 55/2026 — Proiect X",
|
||||
iesit: "ex: Solicitare aviz DSP — Proiect X",
|
||||
},
|
||||
contract: {
|
||||
intrat: "ex: Contract nr. 15/2026 — Proiect X",
|
||||
iesit: "ex: Contract prestari servicii nr. 22/2026 — Proiect X",
|
||||
},
|
||||
oferta: {
|
||||
intrat: "ex: Oferta nr. 8/2026 — Proiect X",
|
||||
iesit: "ex: Oferta proiectare DTAC+PT — Proiect X",
|
||||
},
|
||||
factura: {
|
||||
intrat: "ex: Factura nr. 102/2026 — Proiect X",
|
||||
iesit: "ex: Factura proforma nr. 5/2026 — Proiect X",
|
||||
},
|
||||
scrisoare: {
|
||||
intrat: "ex: Adresa nr. 45/2026 — Raspuns informatii",
|
||||
iesit: "ex: Notificare incepere lucrari — Proiect X",
|
||||
},
|
||||
"nota-de-comanda": {
|
||||
intrat: "ex: Nota de comanda nr. 7/2026 — Proiect X",
|
||||
iesit: "ex: Comanda materiale — Proiect X",
|
||||
},
|
||||
raport: {
|
||||
intrat: "ex: Studiu geotehnic — Proiect X",
|
||||
iesit: "ex: Raport expertiza tehnica — Proiect X",
|
||||
},
|
||||
"apel-telefonic": {
|
||||
intrat: "ex: Apel primarie avize — Proiect X",
|
||||
iesit: "ex: Convorbire ISU programare — Proiect X",
|
||||
},
|
||||
videoconferinta: {
|
||||
intrat: "ex: Sedinta coordonare — Proiect X",
|
||||
iesit: "ex: Videoconferinta revizie PT — Proiect X",
|
||||
},
|
||||
altele: {
|
||||
intrat: "ex: PV receptie nr. 5/2026 — Proiect X",
|
||||
iesit: "ex: Documentatie PAC+DDE — Proiect X",
|
||||
},
|
||||
};
|
||||
|
||||
const DEFAULT_PLACEHOLDER = "Tastati pentru sugestii...";
|
||||
|
||||
/**
|
||||
* Get a dynamic placeholder for the subject input based on document type
|
||||
* and direction.
|
||||
*/
|
||||
export function getSubjectPlaceholder(
|
||||
documentType: string,
|
||||
direction: RegistryDirection,
|
||||
): string {
|
||||
const byType = SUBJECT_PLACEHOLDERS_MAP[documentType];
|
||||
if (!byType) return DEFAULT_PLACEHOLDER;
|
||||
return byType[direction] ?? DEFAULT_PLACEHOLDER;
|
||||
}
|
||||
|
||||
// ── Public API ──
|
||||
|
||||
/**
|
||||
* Get seed templates for a given document type.
|
||||
*/
|
||||
export function getSeedTemplates(documentType: string): SubjectTemplate[] {
|
||||
return SEED_TEMPLATES[documentType] ?? SEED_TEMPLATES["altele"] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recommended templates for a document type: merge seeds + DB-learned.
|
||||
* DB-learned templates take priority (appear first), seeds fill in the rest.
|
||||
* De-duplicates by pattern.
|
||||
*/
|
||||
export function getRecommendedTemplates(
|
||||
documentType: string,
|
||||
dbTemplates: SubjectTemplate[],
|
||||
): SubjectTemplate[] {
|
||||
const seeds = getSeedTemplates(documentType);
|
||||
const seen = new Set<string>();
|
||||
const result: SubjectTemplate[] = [];
|
||||
|
||||
// DB-learned first (higher priority)
|
||||
for (const t of dbTemplates) {
|
||||
if (!seen.has(t.pattern)) {
|
||||
seen.add(t.pattern);
|
||||
result.push(t);
|
||||
}
|
||||
}
|
||||
|
||||
// Then seeds (fill remaining slots)
|
||||
for (const t of seeds) {
|
||||
if (!seen.has(t.pattern)) {
|
||||
seen.add(t.pattern);
|
||||
result.push(t);
|
||||
}
|
||||
}
|
||||
|
||||
return result.slice(0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract reusable templates from an array of existing subjects.
|
||||
* Groups by pattern, counts frequency, returns sorted by frequency desc.
|
||||
|
||||
Reference in New Issue
Block a user