import { useState, useEffect, useCallback } from "react"; import { useNavigate } from "react-router"; import { Masthead } from "../components/Masthead"; import { TemplateEditor } from "../components/templates/TemplateEditor"; import { useAuth } from "../contexts/AuthContext"; import type { ContractTemplate } from "../types/templates"; import { DEFAULT_TEMPLATES } from "../types/templates"; import { listContractTypes, createContractTemplate, updateContractTemplate, deleteContractTemplate, type PhaseDefinition, type DeliverableDefinition, } from "../lib/api"; export default function TemplatesPage() { const navigate = useNavigate(); const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); const [templates, setTemplates] = useState(DEFAULT_TEMPLATES); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [saving, setSaving] = useState(false); const [editingTemplate, setEditingTemplate] = useState( null ); const [showNewTemplateForm, setShowNewTemplateForm] = useState(false); const [newTemplateName, setNewTemplateName] = useState(""); const [newTemplateDescription, setNewTemplateDescription] = useState(""); // Redirect to login if not authenticated useEffect(() => { if (!authLoading && isAuthConfigured && !isAuthenticated) { navigate("/login"); } }, [authLoading, isAuthConfigured, isAuthenticated, navigate]); // Fetch templates from API const fetchTemplates = useCallback(async () => { try { setLoading(true); setError(null); const response = await listContractTypes(); // Convert API response to ContractTemplate format const apiTemplates: ContractTemplate[] = response.contractTypes.map((t) => ({ id: t.id, name: t.name, description: t.description, isBuiltIn: t.isBuiltin, phases: t.phases.map((phaseId) => ({ id: phaseId, name: t.phaseNames?.[phaseId] || phaseId.charAt(0).toUpperCase() + phaseId.slice(1), deliverables: [], // Deliverables are managed server-side })), })); // Merge with DEFAULT_TEMPLATES to ensure we have full phase/deliverable info for built-ins const mergedTemplates = apiTemplates.map((apiTemplate) => { const defaultTemplate = DEFAULT_TEMPLATES.find((d) => d.id === apiTemplate.id); if (defaultTemplate && apiTemplate.isBuiltIn) { return defaultTemplate; // Use the richer default template for built-ins } return apiTemplate; }); setTemplates(mergedTemplates); } catch (err) { console.error("Failed to fetch templates:", err); setError(err instanceof Error ? err.message : "Failed to fetch templates"); // Fall back to default templates setTemplates(DEFAULT_TEMPLATES); } finally { setLoading(false); } }, []); useEffect(() => { if (!authLoading && isAuthenticated) { fetchTemplates(); } else if (!authLoading && !isAuthConfigured) { // No auth configured, just show defaults setTemplates(DEFAULT_TEMPLATES); setLoading(false); } }, [authLoading, isAuthenticated, isAuthConfigured, fetchTemplates]); const handleSaveTemplate = async (updatedTemplate: ContractTemplate) => { if (updatedTemplate.isBuiltIn) { // Built-in templates are read-only, just close the editor setEditingTemplate(null); return; } try { setSaving(true); setError(null); // Convert to API format const phases: PhaseDefinition[] = updatedTemplate.phases.map((p, index) => ({ id: p.id, name: p.name, order: index, })); const deliverables: Record = {}; for (const phase of updatedTemplate.phases) { if (phase.deliverables.length > 0) { deliverables[phase.id] = phase.deliverables.map((d) => ({ id: d.id, name: d.name, priority: "required" as const, })); } } await updateContractTemplate(updatedTemplate.id, { name: updatedTemplate.name, description: updatedTemplate.description, phases, defaultPhase: phases[0]?.id || "execute", deliverables: Object.keys(deliverables).length > 0 ? deliverables : undefined, }); // Refresh templates from server await fetchTemplates(); setEditingTemplate(null); } catch (err) { console.error("Failed to update template:", err); setError(err instanceof Error ? err.message : "Failed to update template"); } finally { setSaving(false); } }; const handleCreateTemplate = async () => { if (!newTemplateName.trim()) return; try { setSaving(true); setError(null); const phases: PhaseDefinition[] = [ { id: "execute", name: "Execute", order: 0 }, ]; await createContractTemplate({ name: newTemplateName.trim(), description: newTemplateDescription.trim() || "Custom contract template", phases, defaultPhase: "execute", }); // Refresh templates from server await fetchTemplates(); setNewTemplateName(""); setNewTemplateDescription(""); setShowNewTemplateForm(false); } catch (err) { console.error("Failed to create template:", err); setError(err instanceof Error ? err.message : "Failed to create template"); } finally { setSaving(false); } }; const handleDeleteTemplate = async (templateId: string) => { const template = templates.find((t) => t.id === templateId); if (template?.isBuiltIn) return; if (window.confirm(`Are you sure you want to delete "${template?.name}"?`)) { try { setSaving(true); setError(null); await deleteContractTemplate(templateId); await fetchTemplates(); } catch (err) { console.error("Failed to delete template:", err); setError(err instanceof Error ? err.message : "Failed to delete template"); } finally { setSaving(false); } } }; const handleRefresh = () => { fetchTemplates(); }; // Show loading state if (authLoading || loading) { return (
Loading...
); } // Editor view if (editingTemplate) { return (
setEditingTemplate(null)} readOnly={editingTemplate.isBuiltIn} />
); } return (
{/* Error display */} {error && (
{error}
)} {/* Header */}

Contract Templates

Manage contract types and their phase deliverables

{/* New Template Form */} {showNewTemplateForm && (
setNewTemplateName(e.target.value)} disabled={saving} /> setNewTemplateDescription(e.target.value)} disabled={saving} />
)} {/* Templates Grid */}
{templates.map((template) => (
{/* Card Header */}

{template.name}

{template.isBuiltIn ? ( Built-in ) : ( Custom )}
{/* Card Body */}

{template.description}

{/* Phases */}
{template.phases.map((phase, index) => (
{index < template.phases.length - 1 && ( )}
{phase.name} {phase.deliverables.length === 0 ? "(no deliverables)" : phase.deliverables.map((d) => d.name).join(", ")}
))}
{/* Card Footer */}
{!template.isBuiltIn && ( )}
))}
); }