Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
128 lines
5.9 KiB
TypeScript
128 lines
5.9 KiB
TypeScript
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
|
import { useState } from "react";
|
|
import { ProtectedLayout } from "@/components/protected-layout";
|
|
import { PageContainer, PageHeader } from "@/components/app-shell";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { supabase } from "@/integrations/supabase/client";
|
|
import { useAuth } from "@/lib/auth";
|
|
import { toast } from "sonner";
|
|
import { Loader2, Plus, Save, Trash2 } from "lucide-react";
|
|
|
|
export const Route = createFileRoute("/documents/templates/new")({
|
|
component: NewTemplatePage,
|
|
});
|
|
|
|
interface FieldDef { key: string; label: string; type: "text" | "textarea" | "date" | "number" }
|
|
|
|
function NewTemplatePage() {
|
|
const { user } = useAuth();
|
|
const navigate = useNavigate();
|
|
const [name, setName] = useState("");
|
|
const [description, setDescription] = useState("");
|
|
const [fields, setFields] = useState<FieldDef[]>([]);
|
|
const [body, setBody] = useState("Dear {{client_name}},\n\n");
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
const addField = () => setFields((f) => [...f, { key: `field_${f.length + 1}`, label: `Field ${f.length + 1}`, type: "text" }]);
|
|
const removeField = (i: number) => setFields((f) => f.filter((_, idx) => idx !== i));
|
|
const updateField = (i: number, patch: Partial<FieldDef>) =>
|
|
setFields((f) => f.map((x, idx) => (idx === i ? { ...x, ...patch } : x)));
|
|
|
|
const insertToken = (key: string) => setBody((b) => b + `{{${key}}}`);
|
|
|
|
const onSave = async () => {
|
|
if (!name.trim()) { toast.error("Name is required"); return; }
|
|
setSaving(true);
|
|
const { data, error } = await supabase.from("document_templates").insert({
|
|
name: name.trim(),
|
|
description: description.trim() || null,
|
|
kind: "general",
|
|
body,
|
|
fields: fields as any,
|
|
created_by: user?.id,
|
|
}).select().single();
|
|
setSaving(false);
|
|
if (error) { toast.error(error.message); return; }
|
|
toast.success("Template created");
|
|
navigate({ to: "/documents/templates/$templateId", params: { templateId: data.id } });
|
|
};
|
|
|
|
return (
|
|
<ProtectedLayout>
|
|
<PageContainer>
|
|
<PageHeader
|
|
title="New template"
|
|
description="Define merge fields and a body. Use {{field_key}} where you want each value inserted."
|
|
actions={
|
|
<Button onClick={onSave} disabled={saving}>
|
|
{saving ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
|
|
Save template
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
<div className="grid lg:grid-cols-2 gap-6">
|
|
<Card>
|
|
<CardContent className="p-5 space-y-4">
|
|
<div className="space-y-1.5">
|
|
<Label>Name</Label>
|
|
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Demand Letter" />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label>Description</Label>
|
|
<Input value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Optional" />
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<Label>Custom fields</Label>
|
|
<Button variant="outline" size="sm" onClick={addField}><Plus className="h-3.5 w-3.5 mr-1" /> Add field</Button>
|
|
</div>
|
|
{fields.length === 0 && <div className="text-xs text-muted-foreground">No fields yet. Add fields like client_name, amount_due, due_date.</div>}
|
|
<div className="space-y-2">
|
|
{fields.map((f, i) => (
|
|
<div key={i} className="grid grid-cols-12 gap-2 items-center">
|
|
<Input className="col-span-4" value={f.key} onChange={(e) => updateField(i, { key: e.target.value.replace(/[^a-zA-Z0-9_]/g, "_") })} placeholder="key" />
|
|
<Input className="col-span-4" value={f.label} onChange={(e) => updateField(i, { label: e.target.value })} placeholder="Label" />
|
|
<select
|
|
className="col-span-3 h-9 rounded-md border border-input bg-transparent px-2 text-sm"
|
|
value={f.type}
|
|
onChange={(e) => updateField(i, { type: e.target.value as any })}
|
|
>
|
|
<option value="text">Text</option>
|
|
<option value="textarea">Long text</option>
|
|
<option value="date">Date</option>
|
|
<option value="number">Number</option>
|
|
</select>
|
|
<Button variant="ghost" size="icon" className="col-span-1" onClick={() => removeField(i)}><Trash2 className="h-4 w-4 text-destructive" /></Button>
|
|
<div className="col-span-12 -mt-1">
|
|
<button type="button" onClick={() => insertToken(f.key)} className="text-[11px] text-primary hover:underline">
|
|
Insert {"{{"}{f.key}{"}}"} into body
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-5 space-y-2">
|
|
<Label>Body</Label>
|
|
<Textarea rows={20} value={body} onChange={(e) => setBody(e.target.value)} className="font-mono text-sm" />
|
|
<p className="text-xs text-muted-foreground">
|
|
Use double curly braces around field keys, e.g. <code>{"{{client_name}}"}</code>. They'll be replaced when generating a document.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</PageContainer>
|
|
</ProtectedLayout>
|
|
);
|
|
}
|