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 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 }) {
|
export function AddDocumentModal({ clientId, onClose }: { clientId: string; onClose: () => void }) {
|
||||||
const [templates, setTemplates] = useState<FormTemplate[]>([]);
|
const [templates, setTemplates] = useState<FormTemplate[]>([]);
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
@@ -14,6 +22,12 @@ export function AddDocumentModal({ clientId, onClose }: { clientId: string; onCl
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const router = useRouter();
|
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(() => {
|
useEffect(() => {
|
||||||
fetch('/api/forms-library')
|
fetch('/api/forms-library')
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
@@ -26,6 +40,7 @@ export function AddDocumentModal({ clientId, onClose }: { clientId: string; onCl
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSelectTemplate = (t: FormTemplate) => {
|
const handleSelectTemplate = (t: FormTemplate) => {
|
||||||
|
setSelectedDocTemplate(null);
|
||||||
setSelectedTemplate(t);
|
setSelectedTemplate(t);
|
||||||
setCustomFile(null);
|
setCustomFile(null);
|
||||||
setDocName(t.name);
|
setDocName(t.name);
|
||||||
@@ -35,16 +50,44 @@ export function AddDocumentModal({ clientId, onClose }: { clientId: string; onCl
|
|||||||
const file = e.target.files?.[0] ?? null;
|
const file = e.target.files?.[0] ?? null;
|
||||||
setCustomFile(file);
|
setCustomFile(file);
|
||||||
setSelectedTemplate(null);
|
setSelectedTemplate(null);
|
||||||
|
setSelectedDocTemplate(null);
|
||||||
if (file) setDocName(file.name.replace(/\.pdf$/i, ''));
|
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) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!docName.trim() || (!selectedTemplate && !customFile)) return;
|
if (!docName.trim() || (!selectedTemplate && !customFile && !selectedDocTemplate)) return;
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
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();
|
const fd = new FormData();
|
||||||
fd.append('clientId', clientId);
|
fd.append('clientId', clientId);
|
||||||
fd.append('name', docName.trim());
|
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">
|
<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>
|
<h2 className="text-xl font-semibold mb-4">Add Document</h2>
|
||||||
|
|
||||||
<input
|
<div className="flex border-b mb-4">
|
||||||
type="text"
|
<button
|
||||||
placeholder="Search forms..."
|
type="button"
|
||||||
value={query}
|
onClick={() => setActiveTab('forms')}
|
||||||
onChange={e => setQuery(e.target.value)}
|
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
|
||||||
className="w-full border rounded px-3 py-2 mb-3 text-sm"
|
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">
|
{activeTab === 'forms' && (
|
||||||
{filtered.length === 0 && (
|
<>
|
||||||
<li className="px-3 py-2 text-sm text-gray-500">No forms found</li>
|
<input
|
||||||
)}
|
type="text"
|
||||||
{filtered.map(t => (
|
placeholder="Search forms..."
|
||||||
<li
|
value={query}
|
||||||
key={t.id}
|
onChange={e => setQuery(e.target.value)}
|
||||||
onClick={() => handleSelectTemplate(t)}
|
className="w-full border rounded px-3 py-2 mb-3 text-sm"
|
||||||
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">
|
<ul className="border rounded max-h-48 overflow-y-auto mb-4">
|
||||||
<label className="block text-sm font-medium mb-1">Or upload a custom PDF</label>
|
{filtered.length === 0 && (
|
||||||
<div className="flex items-center gap-3">
|
<li className="px-3 py-2 text-sm text-gray-500">No forms found</li>
|
||||||
<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
|
{filtered.map(t => (
|
||||||
<input type="file" accept="application/pdf" onChange={handleFileChange} className="hidden" />
|
<li
|
||||||
</label>
|
key={t.id}
|
||||||
{customFile && (
|
onClick={() => handleSelectTemplate(t)}
|
||||||
<span className="text-sm text-gray-600 truncate max-w-xs">{customFile.name}</span>
|
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>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<label className="block text-sm font-medium mb-1">Document name</label>
|
<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>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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"
|
className="px-4 py-2 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{saving ? 'Saving...' : 'Add Document'}
|
{saving ? 'Saving...' : 'Add Document'}
|
||||||
|
|||||||
Reference in New Issue
Block a user