e30b437dce
- 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>
190 lines
5.7 KiB
TypeScript
190 lines
5.7 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState, useMemo } from "react";
|
|
import { X } from "lucide-react";
|
|
import { cn } from "@/shared/lib/utils";
|
|
import type { SubjectTemplate } from "../services/subject-template-service";
|
|
import type { Tag } from "@/core/tagging/types";
|
|
|
|
interface SubjectTemplateInputProps {
|
|
template: SubjectTemplate;
|
|
fieldValues: Record<string, string>;
|
|
onFieldChange: (fieldId: string, value: string) => void;
|
|
onClear: () => void;
|
|
/** Project tags for {proiect} field autocomplete */
|
|
projectTags?: Tag[];
|
|
}
|
|
|
|
export function SubjectTemplateInput({
|
|
template,
|
|
fieldValues,
|
|
onFieldChange,
|
|
onClear,
|
|
projectTags,
|
|
}: SubjectTemplateInputProps) {
|
|
const firstFieldRef = useRef<HTMLInputElement>(null);
|
|
|
|
// Auto-focus first field on mount
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => firstFieldRef.current?.focus(), 50);
|
|
return () => clearTimeout(timer);
|
|
}, []);
|
|
|
|
let fieldRendered = 0;
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex flex-wrap items-baseline gap-0.5",
|
|
"min-h-9 w-full rounded-md border border-input bg-transparent px-3 py-1.5",
|
|
"text-sm shadow-xs mt-1",
|
|
)}
|
|
>
|
|
{template.tokens.map((token, i) => {
|
|
if (token.type === "static") {
|
|
return (
|
|
<span
|
|
key={`s${i}`}
|
|
className="whitespace-nowrap text-muted-foreground"
|
|
>
|
|
{token.value}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
const field = token.field!;
|
|
const isFirst = fieldRendered === 0;
|
|
fieldRendered++;
|
|
|
|
// Project field — render with mini-autocomplete
|
|
if (field.fieldType === "proiect" && projectTags && projectTags.length > 0) {
|
|
return (
|
|
<ProjectFieldInput
|
|
key={field.id}
|
|
field={field}
|
|
value={fieldValues[field.id] ?? ""}
|
|
onChange={(val) => onFieldChange(field.id, val)}
|
|
projectTags={projectTags}
|
|
inputRef={isFirst ? firstFieldRef : undefined}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<input
|
|
key={field.id}
|
|
ref={isFirst ? firstFieldRef : undefined}
|
|
value={fieldValues[field.id] ?? ""}
|
|
onChange={(e) => onFieldChange(field.id, e.target.value)}
|
|
placeholder={field.placeholder}
|
|
className={cn(
|
|
"border-b-2 border-dashed border-primary/40 bg-transparent",
|
|
"text-sm font-medium text-foreground",
|
|
"px-1 py-0 outline-none",
|
|
"focus:border-primary focus:border-solid",
|
|
"placeholder:text-muted-foreground/50 placeholder:italic placeholder:text-xs",
|
|
field.width,
|
|
)}
|
|
/>
|
|
);
|
|
})}
|
|
<button
|
|
type="button"
|
|
onClick={onClear}
|
|
className="ml-auto shrink-0 text-muted-foreground hover:text-foreground transition-colors"
|
|
title="Renunta la sablon"
|
|
>
|
|
<X className="h-3.5 w-3.5" />
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Project Field with mini-autocomplete ──
|
|
|
|
interface ProjectFieldInputProps {
|
|
field: { id: string; placeholder: string; width: string };
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
projectTags: Tag[];
|
|
inputRef?: React.Ref<HTMLInputElement>;
|
|
}
|
|
|
|
function ProjectFieldInput({
|
|
field,
|
|
value,
|
|
onChange,
|
|
projectTags,
|
|
inputRef,
|
|
}: ProjectFieldInputProps) {
|
|
const [focused, setFocused] = useState(false);
|
|
|
|
const suggestions = useMemo(() => {
|
|
if (!value || value.length < 1) return projectTags.slice(0, 8);
|
|
const q = value.toLowerCase();
|
|
return projectTags
|
|
.filter(
|
|
(t) =>
|
|
t.label.toLowerCase().includes(q) ||
|
|
(t.projectCode && t.projectCode.toLowerCase().includes(q)),
|
|
)
|
|
.slice(0, 8);
|
|
}, [value, projectTags]);
|
|
|
|
const showDropdown = focused && suggestions.length > 0;
|
|
|
|
return (
|
|
<span className="relative inline-flex">
|
|
<input
|
|
ref={inputRef}
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
onFocus={() => setFocused(true)}
|
|
onBlur={() => setTimeout(() => setFocused(false), 200)}
|
|
placeholder={field.placeholder}
|
|
className={cn(
|
|
"border-b-2 border-dashed border-primary/40 bg-transparent",
|
|
"text-sm font-medium text-foreground",
|
|
"px-1 py-0 outline-none",
|
|
"focus:border-primary focus:border-solid",
|
|
"placeholder:text-muted-foreground/50 placeholder:italic placeholder:text-xs",
|
|
field.width,
|
|
)}
|
|
/>
|
|
{showDropdown && (
|
|
<div className="absolute top-full left-0 z-20 mt-1 min-w-48 rounded-md border bg-popover p-1 shadow-md max-h-48 overflow-y-auto">
|
|
{suggestions.map((tag) => (
|
|
<button
|
|
key={tag.id}
|
|
type="button"
|
|
className="w-full rounded px-2 py-1 text-left text-xs hover:bg-accent flex items-center gap-1.5"
|
|
onMouseDown={() => {
|
|
const display = tag.projectCode
|
|
? `${tag.projectCode} ${tag.label}`
|
|
: tag.label;
|
|
onChange(display);
|
|
setFocused(false);
|
|
}}
|
|
>
|
|
{tag.color && (
|
|
<span
|
|
className="inline-block h-2.5 w-2.5 rounded-full shrink-0"
|
|
style={{ backgroundColor: tag.color }}
|
|
/>
|
|
)}
|
|
<span className="truncate">
|
|
{tag.projectCode && (
|
|
<span className="font-mono text-muted-foreground mr-1">
|
|
{tag.projectCode}
|
|
</span>
|
|
)}
|
|
{tag.label}
|
|
</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</span>
|
|
);
|
|
}
|