summaryrefslogtreecommitdiff
path: root/makima/frontend/src/routes/templates.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/routes/templates.tsx')
-rw-r--r--makima/frontend/src/routes/templates.tsx248
1 files changed, 184 insertions, 64 deletions
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>