diff options
Diffstat (limited to 'makima/frontend/src/components/templates')
| -rw-r--r-- | makima/frontend/src/components/templates/TemplateEditor.tsx | 248 |
1 files changed, 248 insertions, 0 deletions
diff --git a/makima/frontend/src/components/templates/TemplateEditor.tsx b/makima/frontend/src/components/templates/TemplateEditor.tsx new file mode 100644 index 0000000..03382f3 --- /dev/null +++ b/makima/frontend/src/components/templates/TemplateEditor.tsx @@ -0,0 +1,248 @@ +import { useState } from "react"; +import type { ContractTemplate, Phase, Deliverable } from "../../types/templates"; + +interface Props { + template: ContractTemplate; + onSave: (template: ContractTemplate) => void; + onCancel: () => void; +} + +export function TemplateEditor({ template, onSave, onCancel }: Props) { + const [editedTemplate, setEditedTemplate] = useState<ContractTemplate>({ + ...template, + phases: template.phases.map((p) => ({ + ...p, + deliverables: [...p.deliverables], + })), + }); + const [newDeliverableName, setNewDeliverableName] = useState<{ + [phaseId: string]: string; + }>({}); + + const handlePhaseNameChange = (phaseId: string, newName: string) => { + setEditedTemplate((prev) => ({ + ...prev, + phases: prev.phases.map((p) => + p.id === phaseId ? { ...p, name: newName } : p + ), + })); + }; + + const handleDeliverableNameChange = ( + phaseId: string, + deliverableId: string, + newName: string + ) => { + setEditedTemplate((prev) => ({ + ...prev, + phases: prev.phases.map((p) => + p.id === phaseId + ? { + ...p, + deliverables: p.deliverables.map((d) => + d.id === deliverableId ? { ...d, name: newName } : d + ), + } + : p + ), + })); + }; + + const handleAddDeliverable = (phaseId: string) => { + const name = newDeliverableName[phaseId]?.trim(); + if (!name) return; + + const newDeliverable: Deliverable = { + id: `deliverable-${Date.now()}`, + name, + }; + + setEditedTemplate((prev) => ({ + ...prev, + phases: prev.phases.map((p) => + p.id === phaseId + ? { ...p, deliverables: [...p.deliverables, newDeliverable] } + : p + ), + })); + setNewDeliverableName((prev) => ({ ...prev, [phaseId]: "" })); + }; + + const handleRemoveDeliverable = (phaseId: string, deliverableId: string) => { + setEditedTemplate((prev) => ({ + ...prev, + phases: prev.phases.map((p) => + p.id === phaseId + ? { + ...p, + deliverables: p.deliverables.filter((d) => d.id !== deliverableId), + } + : p + ), + })); + }; + + const handleAddPhase = () => { + const newPhase: Phase = { + id: `phase-${Date.now()}`, + name: "New Phase", + deliverables: [], + }; + setEditedTemplate((prev) => ({ + ...prev, + phases: [...prev.phases, newPhase], + })); + }; + + const handleRemovePhase = (phaseId: string) => { + setEditedTemplate((prev) => ({ + ...prev, + phases: prev.phases.filter((p) => p.id !== phaseId), + })); + }; + + return ( + <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] p-6"> + {/* Header */} + <div className="mb-6 pb-4 border-b border-[rgba(117,170,252,0.15)]"> + <h2 className="text-sm font-mono uppercase tracking-wide text-[#9bc3ff] mb-1"> + Edit Template: {template.name} + </h2> + <p className="text-xs font-mono text-[#75aafc] opacity-70"> + {template.description} + </p> + </div> + + {/* Phases */} + <div className="space-y-4 mb-6"> + {editedTemplate.phases.map((phase, phaseIndex) => ( + <div + key={phase.id} + className="bg-[rgba(0,0,0,0.3)] border border-[rgba(117,170,252,0.2)] p-4" + > + {/* Phase Header */} + <div className="flex items-center gap-3 mb-3"> + <span className="w-6 h-6 flex items-center justify-center bg-[rgba(117,170,252,0.2)] text-[#9bc3ff] text-xs font-mono"> + {phaseIndex + 1} + </span> + <input + type="text" + className="flex-1 px-3 py-1.5 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3]" + value={phase.name} + onChange={(e) => handlePhaseNameChange(phase.id, e.target.value)} + placeholder="Phase name" + /> + {!template.isBuiltIn && ( + <button + type="button" + onClick={() => handleRemovePhase(phase.id)} + className="w-7 h-7 flex items-center justify-center border border-[rgba(255,100,100,0.3)] text-[#ff6464] hover:bg-[rgba(255,100,100,0.1)] transition-colors text-sm" + title="Remove phase" + > + x + </button> + )} + </div> + + {/* Deliverables */} + <div className="ml-9 space-y-2"> + {phase.deliverables.length === 0 ? ( + <div className="text-xs font-mono text-[#556677] italic"> + No deliverables + </div> + ) : ( + phase.deliverables.map((deliverable) => ( + <div + key={deliverable.id} + className="flex items-center gap-2" + > + <span className="text-[#75aafc] text-xs">-</span> + <input + type="text" + className="flex-1 px-2 py-1 bg-transparent border border-[rgba(117,170,252,0.15)] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#3f6fb3]" + value={deliverable.name} + onChange={(e) => + handleDeliverableNameChange( + phase.id, + deliverable.id, + e.target.value + ) + } + /> + <button + type="button" + onClick={() => + handleRemoveDeliverable(phase.id, deliverable.id) + } + className="w-5 h-5 flex items-center justify-center text-[#ff6464] hover:bg-[rgba(255,100,100,0.1)] transition-colors text-xs" + title="Remove deliverable" + > + x + </button> + </div> + )) + )} + + {/* Add Deliverable */} + <div className="flex items-center gap-2 pt-2"> + <input + type="text" + className="flex-1 px-2 py-1 bg-transparent border border-[rgba(117,170,252,0.15)] text-[#dbe7ff] font-mono text-xs placeholder-[#445566] focus:outline-none focus:border-[#3f6fb3]" + placeholder="New deliverable name..." + value={newDeliverableName[phase.id] || ""} + onChange={(e) => + setNewDeliverableName((prev) => ({ + ...prev, + [phase.id]: e.target.value, + })) + } + onKeyPress={(e) => { + if (e.key === "Enter") { + handleAddDeliverable(phase.id); + } + }} + /> + <button + type="button" + onClick={() => handleAddDeliverable(phase.id)} + className="px-2 py-1 border border-[rgba(117,170,252,0.3)] text-[#9bc3ff] font-mono text-xs hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.1)] transition-colors" + > + + Add + </button> + </div> + </div> + </div> + ))} + </div> + + {/* Add Phase (only for custom templates) */} + {!template.isBuiltIn && ( + <button + type="button" + onClick={handleAddPhase} + className="w-full mb-6 px-4 py-2 border border-dashed border-[rgba(117,170,252,0.3)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors" + > + + Add Phase + </button> + )} + + {/* Footer Actions */} + <div className="flex gap-3 justify-end pt-4 border-t border-[rgba(117,170,252,0.15)]"> + <button + type="button" + onClick={onCancel} + 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> + <button + type="button" + onClick={() => onSave(editedTemplate)} + 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" + > + Save Changes + </button> + </div> + </div> + ); +} |
