From f19acd400cc5bbe1fe51c004c50ee90d704240d8 Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Thu, 29 Jan 2026 02:56:44 +0000
Subject: Fix contract type selection

---
 makima/frontend/src/routes/templates.tsx | 248 +++++++++++++++++++++++--------
 1 file changed, 184 insertions(+), 64 deletions(-)

(limited to 'makima/frontend/src/routes/templates.tsx')

diff --git a/makima/frontend/src/routes/templates.tsx b/makima/frontend/src/routes/templates.tsx
index 15bf95c..b2c9974 100644
--- a/makima/frontend/src/routes/templates.tsx
+++ b/makima/frontend/src/routes/templates.tsx
@@ -1,28 +1,27 @@
-import { useState, useEffect } from "react";
+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";
-
-const STORAGE_KEY = "makima_contract_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[]>(() => {
-    const saved = localStorage.getItem(STORAGE_KEY);
-    if (saved) {
-      try {
-        return JSON.parse(saved);
-      } catch {
-        return DEFAULT_TEMPLATES;
-      }
-    }
-    return DEFAULT_TEMPLATES;
-  });
+  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
@@ -38,63 +37,161 @@ export default function TemplatesPage() {
     }
   }, [authLoading, isAuthConfigured, isAuthenticated, navigate]);
 
-  const saveTemplates = (newTemplates: ContractTemplate[]) => {
-    setTemplates(newTemplates);
-    localStorage.setItem(STORAGE_KEY, JSON.stringify(newTemplates));
-  };
+  // Fetch templates from API
+  const fetchTemplates = useCallback(async () => {
+    try {
+      setLoading(true);
+      setError(null);
+      const response = await listContractTypes();
 
-  const handleSaveTemplate = (updatedTemplate: ContractTemplate) => {
-    const newTemplates = templates.map((t) =>
-      t.id === updatedTemplate.id ? updatedTemplate : t
-    );
-    saveTemplates(newTemplates);
-    setEditingTemplate(null);
+      // 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 = () => {
+  const handleCreateTemplate = async () => {
     if (!newTemplateName.trim()) return;
 
-    const newTemplate: ContractTemplate = {
-      id: `custom-${Date.now()}`,
-      name: newTemplateName.trim(),
-      description: newTemplateDescription.trim() || "Custom contract template",
-      isBuiltIn: false,
-      phases: [
-        {
-          id: `phase-${Date.now()}`,
-          name: "Execute",
-          deliverables: [],
-        },
-      ],
-    };
-
-    saveTemplates([...templates, newTemplate]);
-    setNewTemplateName("");
-    setNewTemplateDescription("");
-    setShowNewTemplateForm(false);
+    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 = (templateId: string) => {
+  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}"?`)) {
-      saveTemplates(templates.filter((t) => t.id !== templateId));
+      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 handleResetToDefaults = () => {
-    if (
-      window.confirm(
-        "Reset all templates to defaults? This will remove any custom templates."
-      )
-    ) {
-      saveTemplates(DEFAULT_TEMPLATES);
-    }
+  const handleRefresh = () => {
+    fetchTemplates();
   };
 
   // Show loading state
-  if (authLoading) {
+  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">
@@ -114,6 +211,7 @@ export default function TemplatesPage() {
             template={editingTemplate}
             onSave={handleSaveTemplate}
             onCancel={() => setEditingTemplate(null)}
+            readOnly={editingTemplate.isBuiltIn}
           />
         </main>
       </div>
@@ -124,6 +222,20 @@ export default function TemplatesPage() {
     <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>
@@ -137,15 +249,17 @@ export default function TemplatesPage() {
           <div className="flex gap-3">
             <button
               type="button"
-              onClick={handleResetToDefaults}
-              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"
+              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"
             >
-              Reset to Defaults
+              Refresh
             </button>
             <button
               type="button"
               onClick={() => setShowNewTemplateForm(true)}
-              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={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>
@@ -162,6 +276,7 @@ export default function TemplatesPage() {
                 placeholder="Template name..."
                 value={newTemplateName}
                 onChange={(e) => setNewTemplateName(e.target.value)}
+                disabled={saving}
               />
               <input
                 type="text"
@@ -169,18 +284,21 @@ export default function TemplatesPage() {
                 placeholder="Description (optional)..."
                 value={newTemplateDescription}
                 onChange={(e) => setNewTemplateDescription(e.target.value)}
+                disabled={saving}
               />
               <button
                 type="button"
                 onClick={handleCreateTemplate}
-                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={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"
               >
-                Create
+                {saving ? "Creating..." : "Create"}
               </button>
               <button
                 type="button"
                 onClick={() => setShowNewTemplateForm(false)}
-                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={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>
@@ -245,15 +363,17 @@ export default function TemplatesPage() {
                 <button
                   type="button"
                   onClick={() => setEditingTemplate(template)}
-                  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={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"
                 >
-                  Edit
+                  {template.isBuiltIn ? "View" : "Edit"}
                 </button>
                 {!template.isBuiltIn && (
                   <button
                     type="button"
                     onClick={() => handleDeleteTemplate(template.id)}
-                    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={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>
-- 
cgit v1.2.3

