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<ContractTemplate[]>(DEFAULT_TEMPLATES);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const [editingTemplate, setEditingTemplate] = useState<ContractTemplate | null>(
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<string, DeliverableDefinition[]> = {};
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 (
<div className="relative z-10 min-h-screen flex items-center justify-center bg-[#0a1628]">
<div className="text-[#75aafc] font-mono text-sm animate-pulse">
Loading...
</div>
</div>
);
}
// Editor view
if (editingTemplate) {
return (
<div className="relative z-10 min-h-screen bg-[#0a1628]">
<Masthead />
<main className="max-w-4xl mx-auto px-4 py-6">
<TemplateEditor
template={editingTemplate}
onSave={handleSaveTemplate}
onCancel={() => setEditingTemplate(null)}
readOnly={editingTemplate.isBuiltIn}
/>
</main>
</div>
);
}
return (
<div className="relative z-10 min-h-screen bg-[#0a1628]">
<Masthead />
<main className="max-w-6xl mx-auto px-4 py-6">
{/* Error display */}
{error && (
<div className="mb-4 p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-xs">
{error}
<button
type="button"
onClick={() => setError(null)}
className="ml-2 text-red-400/70 hover:text-red-400"
>
Dismiss
</button>
</div>
)}
{/* Header */}
<div className="flex justify-between items-start mb-6 pb-4 border-b border-[rgba(117,170,252,0.15)]">
<div>
<h1 className="text-lg font-mono uppercase tracking-wide text-[#9bc3ff] mb-1">
Contract Templates
</h1>
<p className="text-xs font-mono text-[#75aafc] opacity-70">
Manage contract types and their phase deliverables
</p>
</div>
<div className="flex gap-3">
<button
type="button"
onClick={handleRefresh}
disabled={saving}
className="px-3 py-2 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] transition-colors disabled:opacity-50"
>
Refresh
</button>
<button
type="button"
onClick={() => setShowNewTemplateForm(true)}
disabled={saving}
className="px-3 py-2 border border-[#3f6fb3] bg-[rgba(117,170,252,0.15)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:bg-[rgba(117,170,252,0.25)] transition-colors disabled:opacity-50"
>
+ New Template
</button>
</div>
</div>
{/* New Template Form */}
{showNewTemplateForm && (
<div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] p-4 mb-6">
<div className="flex gap-3 items-center">
<input
type="text"
className="flex-1 px-3 py-2 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3]"
placeholder="Template name..."
value={newTemplateName}
onChange={(e) => setNewTemplateName(e.target.value)}
disabled={saving}
/>
<input
type="text"
className="flex-1 px-3 py-2 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3]"
placeholder="Description (optional)..."
value={newTemplateDescription}
onChange={(e) => setNewTemplateDescription(e.target.value)}
disabled={saving}
/>
<button
type="button"
onClick={handleCreateTemplate}
disabled={saving || !newTemplateName.trim()}
className="px-4 py-2 border border-[#3f6fb3] bg-[rgba(117,170,252,0.15)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:bg-[rgba(117,170,252,0.25)] transition-colors disabled:opacity-50"
>
{saving ? "Creating..." : "Create"}
</button>
<button
type="button"
onClick={() => setShowNewTemplateForm(false)}
disabled={saving}
className="px-4 py-2 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] transition-colors disabled:opacity-50"
>
Cancel
</button>
</div>
</div>
)}
{/* Templates Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{templates.map((template) => (
<div
key={template.id}
className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.25)] hover:border-[rgba(117,170,252,0.45)] transition-colors"
>
{/* Card Header */}
<div className="px-4 py-3 border-b border-[rgba(117,170,252,0.15)] flex items-center justify-between">
<h3 className="font-mono text-sm text-white">{template.name}</h3>
{template.isBuiltIn ? (
<span className="px-2 py-0.5 bg-[rgba(117,170,252,0.15)] text-[#75aafc] font-mono text-[10px] uppercase tracking-wide">
Built-in
</span>
) : (
<span className="px-2 py-0.5 bg-[rgba(100,200,100,0.15)] text-[#7bc97b] font-mono text-[10px] uppercase tracking-wide">
Custom
</span>
)}
</div>
{/* Card Body */}
<div className="px-4 py-3">
<p className="text-xs font-mono text-[#75aafc] opacity-70 mb-4 min-h-[2.5rem]">
{template.description}
</p>
{/* Phases */}
<div className="space-y-2 mb-4">
{template.phases.map((phase, index) => (
<div key={phase.id} className="flex items-start gap-2">
<div className="flex flex-col items-center">
<span className="w-2 h-2 rounded-full bg-[#3f6fb3]" />
{index < template.phases.length - 1 && (
<span className="w-px h-4 bg-[rgba(117,170,252,0.25)]" />
)}
</div>
<div className="flex-1 min-w-0">
<span className="text-xs font-mono text-[#9bc3ff]">
{phase.name}
</span>
<span className="text-[10px] font-mono text-[#556677] ml-2">
{phase.deliverables.length === 0
? "(no deliverables)"
: phase.deliverables.map((d) => d.name).join(", ")}
</span>
</div>
</div>
))}
</div>
</div>
{/* Card Footer */}
<div className="px-4 py-3 border-t border-[rgba(117,170,252,0.15)] flex gap-2">
<button
type="button"
onClick={() => setEditingTemplate(template)}
disabled={saving}
className="flex-1 px-3 py-1.5 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors disabled:opacity-50"
>
{template.isBuiltIn ? "View" : "Edit"}
</button>
{!template.isBuiltIn && (
<button
type="button"
onClick={() => handleDeleteTemplate(template.id)}
disabled={saving}
className="px-3 py-1.5 border border-[rgba(255,100,100,0.25)] text-[#ff6464] font-mono text-xs uppercase tracking-wide hover:border-[rgba(255,100,100,0.5)] hover:bg-[rgba(255,100,100,0.05)] transition-colors disabled:opacity-50"
>
Delete
</button>
)}
</div>
</div>
))}
</div>
</main>
</div>
);
}