summaryrefslogblamecommitdiff
path: root/makima/frontend/src/routes/templates.tsx
blob: 15bf95c6d306d052537d5730b4bb3cf59cf31e4a (plain) (tree)

































































































                                                                                  
                                                                                                









                                                                        
                                                               












                                                      
                                                             















































































































































                                                                                                                                                                                                                                    
import { useState, useEffect } 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";

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 [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]);

  const saveTemplates = (newTemplates: ContractTemplate[]) => {
    setTemplates(newTemplates);
    localStorage.setItem(STORAGE_KEY, JSON.stringify(newTemplates));
  };

  const handleSaveTemplate = (updatedTemplate: ContractTemplate) => {
    const newTemplates = templates.map((t) =>
      t.id === updatedTemplate.id ? updatedTemplate : t
    );
    saveTemplates(newTemplates);
    setEditingTemplate(null);
  };

  const handleCreateTemplate = () => {
    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);
  };

  const handleDeleteTemplate = (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));
    }
  };

  const handleResetToDefaults = () => {
    if (
      window.confirm(
        "Reset all templates to defaults? This will remove any custom templates."
      )
    ) {
      saveTemplates(DEFAULT_TEMPLATES);
    }
  };

  // Show loading state
  if (authLoading) {
    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)}
          />
        </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">
        {/* 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={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"
            >
              Reset to Defaults
            </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"
            >
              + 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)}
              />
              <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)}
              />
              <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"
              >
                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"
              >
                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)}
                  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"
                >
                  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"
                  >
                    Delete
                  </button>
                )}
              </div>
            </div>
          ))}
        </div>
      </main>
    </div>
  );
}