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:
Chandler Copeland
2026-04-06 14:51:23 -06:00
parent bdf0cb02ff
commit 2947fa558c

View File

@@ -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'} &middot; {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'}