feat(20-01): add My Templates tab to AddDocumentModal
- Add DocumentTemplateRow type and activeTab/docTemplates/selectedDocTemplate state - Lazy-fetch /api/templates on first My Templates tab click - handleSwitchToTemplates lazy-loads on first click only - handleSelectDocTemplate clears selectedTemplate and customFile (mutual exclusivity) - handleSelectTemplate now also clears selectedDocTemplate - handleSubmit: new branch at top sends documentTemplateId to POST /api/documents - Guard and disabled condition updated to include selectedDocTemplate - Tab bar renders with underline-style active indicator matching project Tailwind patterns - Existing Forms Library content and custom upload section wrapped in activeTab === 'forms' conditional
This commit is contained in:
@@ -4,6 +4,14 @@ import { useRouter } from 'next/navigation';
|
||||
|
||||
type FormTemplate = { id: string; name: string; filename: string };
|
||||
|
||||
type DocumentTemplateRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
formName: string | null;
|
||||
fieldCount: number;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export function AddDocumentModal({ clientId, onClose }: { clientId: string; onClose: () => void }) {
|
||||
const [templates, setTemplates] = useState<FormTemplate[]>([]);
|
||||
const [query, setQuery] = useState('');
|
||||
@@ -14,6 +22,12 @@ export function AddDocumentModal({ clientId, onClose }: { clientId: string; onCl
|
||||
const [saving, setSaving] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
// Template tab state
|
||||
const [activeTab, setActiveTab] = useState<'forms' | 'templates'>('forms');
|
||||
const [docTemplates, setDocTemplates] = useState<DocumentTemplateRow[]>([]);
|
||||
const [docTemplatesLoaded, setDocTemplatesLoaded] = useState(false);
|
||||
const [selectedDocTemplate, setSelectedDocTemplate] = useState<DocumentTemplateRow | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/forms-library')
|
||||
.then(r => r.json())
|
||||
@@ -26,6 +40,7 @@ export function AddDocumentModal({ clientId, onClose }: { clientId: string; onCl
|
||||
);
|
||||
|
||||
const handleSelectTemplate = (t: FormTemplate) => {
|
||||
setSelectedDocTemplate(null);
|
||||
setSelectedTemplate(t);
|
||||
setCustomFile(null);
|
||||
setDocName(t.name);
|
||||
@@ -35,16 +50,44 @@ export function AddDocumentModal({ clientId, onClose }: { clientId: string; onCl
|
||||
const file = e.target.files?.[0] ?? null;
|
||||
setCustomFile(file);
|
||||
setSelectedTemplate(null);
|
||||
setSelectedDocTemplate(null);
|
||||
if (file) setDocName(file.name.replace(/\.pdf$/i, ''));
|
||||
};
|
||||
|
||||
function handleSwitchToTemplates() {
|
||||
setActiveTab('templates');
|
||||
if (!docTemplatesLoaded) {
|
||||
fetch('/api/templates')
|
||||
.then(r => r.json())
|
||||
.then((data: DocumentTemplateRow[]) => { setDocTemplates(data); setDocTemplatesLoaded(true); })
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectDocTemplate = (t: DocumentTemplateRow) => {
|
||||
setSelectedDocTemplate(t);
|
||||
setSelectedTemplate(null);
|
||||
setCustomFile(null);
|
||||
setDocName(t.name);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!docName.trim() || (!selectedTemplate && !customFile)) return;
|
||||
if (!docName.trim() || (!selectedTemplate && !customFile && !selectedDocTemplate)) return;
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
if (customFile) {
|
||||
if (selectedDocTemplate) {
|
||||
await fetch('/api/documents', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
clientId,
|
||||
name: docName.trim(),
|
||||
documentTemplateId: selectedDocTemplate.id,
|
||||
}),
|
||||
});
|
||||
} else if (customFile) {
|
||||
const fd = new FormData();
|
||||
fd.append('clientId', clientId);
|
||||
fd.append('name', docName.trim());
|
||||
@@ -71,41 +114,99 @@ export function AddDocumentModal({ clientId, onClose }: { clientId: string; onCl
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4 p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Add Document</h2>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search forms..."
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
className="w-full border rounded px-3 py-2 mb-3 text-sm"
|
||||
/>
|
||||
<div className="flex border-b mb-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab('forms')}
|
||||
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
|
||||
activeTab === 'forms'
|
||||
? 'border-blue-600 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Forms Library
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSwitchToTemplates}
|
||||
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
|
||||
activeTab === 'templates'
|
||||
? 'border-blue-600 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
My Templates
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul className="border rounded max-h-48 overflow-y-auto mb-4">
|
||||
{filtered.length === 0 && (
|
||||
<li className="px-3 py-2 text-sm text-gray-500">No forms found</li>
|
||||
)}
|
||||
{filtered.map(t => (
|
||||
<li
|
||||
key={t.id}
|
||||
onClick={() => handleSelectTemplate(t)}
|
||||
className={`px-3 py-2 text-sm cursor-pointer hover:bg-gray-100 ${selectedTemplate?.id === t.id ? 'bg-blue-50 font-medium' : ''}`}
|
||||
>
|
||||
{t.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{activeTab === 'forms' && (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search forms..."
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
className="w-full border rounded px-3 py-2 mb-3 text-sm"
|
||||
/>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">Or upload a custom PDF</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="cursor-pointer px-4 py-2 text-sm border border-gray-300 rounded bg-white hover:bg-gray-50 font-medium">
|
||||
Browse files
|
||||
<input type="file" accept="application/pdf" onChange={handleFileChange} className="hidden" />
|
||||
</label>
|
||||
{customFile && (
|
||||
<span className="text-sm text-gray-600 truncate max-w-xs">{customFile.name}</span>
|
||||
<ul className="border rounded max-h-48 overflow-y-auto mb-4">
|
||||
{filtered.length === 0 && (
|
||||
<li className="px-3 py-2 text-sm text-gray-500">No forms found</li>
|
||||
)}
|
||||
{filtered.map(t => (
|
||||
<li
|
||||
key={t.id}
|
||||
onClick={() => handleSelectTemplate(t)}
|
||||
className={`px-3 py-2 text-sm cursor-pointer hover:bg-gray-100 ${selectedTemplate?.id === t.id ? 'bg-blue-50 font-medium' : ''}`}
|
||||
>
|
||||
{t.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">Or upload a custom PDF</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="cursor-pointer px-4 py-2 text-sm border border-gray-300 rounded bg-white hover:bg-gray-50 font-medium">
|
||||
Browse files
|
||||
<input type="file" accept="application/pdf" onChange={handleFileChange} className="hidden" />
|
||||
</label>
|
||||
{customFile && (
|
||||
<span className="text-sm text-gray-600 truncate max-w-xs">{customFile.name}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'templates' && (
|
||||
<div className="mb-4">
|
||||
{!docTemplatesLoaded ? (
|
||||
<p className="text-sm text-gray-500 py-4 text-center">Loading templates...</p>
|
||||
) : docTemplates.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 py-4 text-center">
|
||||
No templates saved yet. Create one from the Templates page.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="border rounded max-h-48 overflow-y-auto">
|
||||
{docTemplates.map(t => (
|
||||
<li
|
||||
key={t.id}
|
||||
onClick={() => handleSelectDocTemplate(t)}
|
||||
className={`px-3 py-2 text-sm cursor-pointer hover:bg-gray-100 ${
|
||||
selectedDocTemplate?.id === t.id ? 'bg-blue-50 font-medium' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="block">{t.name}</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{t.formName ?? 'Unknown form'} · {t.fieldCount} field{t.fieldCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label className="block text-sm font-medium mb-1">Document name</label>
|
||||
@@ -124,7 +225,7 @@ export function AddDocumentModal({ clientId, onClose }: { clientId: string; onCl
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving || (!selectedTemplate && !customFile) || !docName.trim()}
|
||||
disabled={saving || (!selectedTemplate && !customFile && !selectedDocTemplate) || !docName.trim()}
|
||||
className="px-4 py-2 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{saving ? 'Saving...' : 'Add Document'}
|
||||
|
||||
Reference in New Issue
Block a user