Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
2236a45d91
commit
3483e065ec
@@ -0,0 +1,127 @@
|
||||
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,
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user