Files
mylegal-stage-law/src/routes/settings.index.tsx
T
2026-04-17 00:52:38 +00:00

348 lines
11 KiB
TypeScript

import { createFileRoute } from "@tanstack/react-router";
import { useEffect, useRef, useState } from "react";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/lib/auth";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Loader2, Upload, Trash2, Building2 } from "lucide-react";
import { toast } from "sonner";
export const Route = createFileRoute("/settings/")({
component: CompanySettingsPage,
});
const EMPTY = {
company_name: "",
contact_email: "",
contact_phone: "",
website: "",
address_line1: "",
address_line2: "",
city: "",
state: "",
postal_code: "",
country: "",
invoice_prefix: "",
default_tax_rate: "",
footer_note: "",
logo_storage_path: "" as string | null | "",
};
function CompanySettingsPage() {
const { user } = useAuth();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [uploading, setUploading] = useState(false);
const [recordId, setRecordId] = useState<string | null>(null);
const [form, setForm] = useState({ ...EMPTY });
const [logoUrl, setLogoUrl] = useState<string | null>(null);
const fileInput = useRef<HTMLInputElement>(null);
const load = async () => {
setLoading(true);
const { data } = await supabase
.from("firm_settings")
.select("*")
.order("updated_at", { ascending: false })
.limit(1)
.maybeSingle();
if (data) {
setRecordId(data.id);
setForm({
company_name: data.company_name ?? "",
contact_email: data.contact_email ?? "",
contact_phone: data.contact_phone ?? "",
website: data.website ?? "",
address_line1: data.address_line1 ?? "",
address_line2: data.address_line2 ?? "",
city: data.city ?? "",
state: data.state ?? "",
postal_code: data.postal_code ?? "",
country: data.country ?? "",
invoice_prefix: data.invoice_prefix ?? "",
default_tax_rate:
data.default_tax_rate != null ? String(data.default_tax_rate) : "",
footer_note: data.footer_note ?? "",
logo_storage_path: data.logo_storage_path ?? "",
});
if (data.logo_storage_path) {
const { data: pub } = supabase.storage
.from("firm-logos")
.getPublicUrl(data.logo_storage_path);
setLogoUrl(pub.publicUrl);
} else {
setLogoUrl(null);
}
}
setLoading(false);
};
useEffect(() => {
load();
}, []);
const update = (k: keyof typeof EMPTY, v: string) =>
setForm((f) => ({ ...f, [k]: v }));
const onLogoFile = async (file: File) => {
setUploading(true);
const ext = file.name.split(".").pop() || "png";
const path = `firm-${Date.now()}.${ext}`;
const { error } = await supabase.storage
.from("firm-logos")
.upload(path, file, { upsert: true, contentType: file.type });
if (error) {
setUploading(false);
toast.error("Upload failed", { description: error.message });
return;
}
setForm((f) => ({ ...f, logo_storage_path: path }));
const { data: pub } = supabase.storage
.from("firm-logos")
.getPublicUrl(path);
setLogoUrl(pub.publicUrl);
setUploading(false);
toast.success("Logo uploaded — remember to save");
};
const removeLogo = () => {
setForm((f) => ({ ...f, logo_storage_path: "" }));
setLogoUrl(null);
};
const save = async () => {
setSaving(true);
const payload: any = {
company_name: form.company_name || null,
contact_email: form.contact_email || null,
contact_phone: form.contact_phone || null,
website: form.website || null,
address_line1: form.address_line1 || null,
address_line2: form.address_line2 || null,
city: form.city || null,
state: form.state || null,
postal_code: form.postal_code || null,
country: form.country || null,
invoice_prefix: form.invoice_prefix || null,
default_tax_rate: form.default_tax_rate
? parseFloat(form.default_tax_rate)
: null,
footer_note: form.footer_note || null,
logo_storage_path: form.logo_storage_path || null,
updated_by: user?.id,
};
const { error } = recordId
? await supabase.from("firm_settings").update(payload).eq("id", recordId)
: await supabase.from("firm_settings").insert(payload);
setSaving(false);
if (error) {
toast.error("Could not save", { description: error.message });
return;
}
toast.success("Settings saved");
load();
};
if (loading) {
return (
<div className="flex justify-center py-16">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="space-y-6 max-w-4xl">
<Card className="border-border/60">
<CardHeader>
<CardTitle className="font-serif text-base flex items-center gap-2">
<Building2 className="h-4 w-4 text-muted-foreground" /> Company logo
</CardTitle>
</CardHeader>
<CardContent className="flex flex-wrap items-center gap-6">
<div className="h-24 w-24 rounded-md border bg-muted/30 flex items-center justify-center overflow-hidden">
{logoUrl ? (
<img
src={logoUrl}
alt="Firm logo"
className="h-full w-full object-contain"
/>
) : (
<Building2 className="h-8 w-8 text-muted-foreground" />
)}
</div>
<div className="flex flex-col gap-2">
<input
ref={fileInput}
type="file"
accept="image/*"
className="hidden"
onChange={(e) => {
const f = e.target.files?.[0];
if (f) onLogoFile(f);
}}
/>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => fileInput.current?.click()}
disabled={uploading}
>
{uploading ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Upload className="h-4 w-4 mr-2" />
)}
Upload logo
</Button>
{logoUrl && (
<Button variant="ghost" size="sm" onClick={removeLogo}>
<Trash2 className="h-4 w-4 mr-2" /> Remove
</Button>
)}
</div>
<p className="text-xs text-muted-foreground">
PNG, JPG or SVG recommended. Max ~2 MB.
</p>
</div>
</CardContent>
</Card>
<Card className="border-border/60">
<CardHeader>
<CardTitle className="font-serif text-base">
Company information
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4 sm:grid-cols-2">
<Field label="Company name">
<Input
value={form.company_name}
onChange={(e) => update("company_name", e.target.value)}
/>
</Field>
<Field label="Website">
<Input
value={form.website}
onChange={(e) => update("website", e.target.value)}
placeholder="https://"
/>
</Field>
<Field label="Contact email">
<Input
type="email"
value={form.contact_email}
onChange={(e) => update("contact_email", e.target.value)}
/>
</Field>
<Field label="Contact phone">
<Input
value={form.contact_phone}
onChange={(e) => update("contact_phone", e.target.value)}
/>
</Field>
<Field label="Address line 1" className="sm:col-span-2">
<Input
value={form.address_line1}
onChange={(e) => update("address_line1", e.target.value)}
/>
</Field>
<Field label="Address line 2" className="sm:col-span-2">
<Input
value={form.address_line2}
onChange={(e) => update("address_line2", e.target.value)}
/>
</Field>
<Field label="City">
<Input
value={form.city}
onChange={(e) => update("city", e.target.value)}
/>
</Field>
<Field label="State / Province">
<Input
value={form.state}
onChange={(e) => update("state", e.target.value)}
/>
</Field>
<Field label="Postal code">
<Input
value={form.postal_code}
onChange={(e) => update("postal_code", e.target.value)}
/>
</Field>
<Field label="Country">
<Input
value={form.country}
onChange={(e) => update("country", e.target.value)}
/>
</Field>
</CardContent>
</Card>
<Card className="border-border/60">
<CardHeader>
<CardTitle className="font-serif text-base">
Invoice defaults
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4 sm:grid-cols-2">
<Field label="Invoice number prefix">
<Input
value={form.invoice_prefix}
onChange={(e) => update("invoice_prefix", e.target.value)}
placeholder="e.g. INV-"
/>
</Field>
<Field label="Default tax rate (%)">
<Input
type="number"
step="0.01"
min="0"
max="100"
value={form.default_tax_rate}
onChange={(e) => update("default_tax_rate", e.target.value)}
/>
</Field>
<Field label="Invoice footer note" className="sm:col-span-2">
<Textarea
rows={3}
value={form.footer_note}
onChange={(e) => update("footer_note", e.target.value)}
placeholder="Thank you for your business…"
/>
</Field>
</CardContent>
</Card>
<div className="flex justify-end">
<Button onClick={save} disabled={saving}>
{saving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Save changes
</Button>
</div>
</div>
);
}
function Field({
label,
className,
children,
}: {
label: string;
className?: string;
children: React.ReactNode;
}) {
return (
<div className={`space-y-1.5 ${className ?? ""}`}>
<Label className="text-xs">{label}</Label>
{children}
</div>
);
}