Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
39b5036d0d
commit
e080623421
@@ -0,0 +1,200 @@
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { useEffect, useMemo, 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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { Loader2, Send, ExternalLink } from "lucide-react";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const Route = createFileRoute("/status/")({
|
||||
component: () => (
|
||||
<ProtectedLayout>
|
||||
<StatusUpdatesPage />
|
||||
</ProtectedLayout>
|
||||
),
|
||||
});
|
||||
|
||||
function nowLocal() {
|
||||
const d = new Date();
|
||||
d.setMinutes(d.getMinutes() - d.getTimezoneOffset());
|
||||
return d.toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
function StatusUpdatesPage() {
|
||||
const { user } = useAuth();
|
||||
const [cases, setCases] = useState<any[]>([]);
|
||||
const [recent, setRecent] = useState<any[]>([]);
|
||||
const [caseId, setCaseId] = useState<string>("");
|
||||
const [title, setTitle] = useState(nowLocal());
|
||||
const [body, setBody] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const loadCases = async () => {
|
||||
const { data, error } = await supabase
|
||||
.from("cases")
|
||||
.select("id, case_number, title, client:clients(name)")
|
||||
.order("opened_at", { ascending: false })
|
||||
.limit(500);
|
||||
if (error) toast.error("Could not load cases", { description: error.message });
|
||||
setCases(data ?? []);
|
||||
};
|
||||
|
||||
const loadRecent = async () => {
|
||||
const { data, error } = await supabase
|
||||
.from("status_updates")
|
||||
.select("*, case:cases(id, case_number, title, client:clients(name))")
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(25);
|
||||
if (error) { toast.error("Could not load updates", { description: error.message }); setLoading(false); return; }
|
||||
const ids = Array.from(new Set((data ?? []).map((d: any) => d.created_by).filter(Boolean)));
|
||||
let profMap: Record<string, any> = {};
|
||||
if (ids.length) {
|
||||
const { data: profs } = await supabase.from("profiles").select("id, full_name, email").in("id", ids);
|
||||
profMap = Object.fromEntries((profs ?? []).map((p: any) => [p.id, p]));
|
||||
}
|
||||
setRecent((data ?? []).map((u: any) => ({ ...u, user: u.created_by ? profMap[u.created_by] : null })));
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => { loadCases(); loadRecent(); }, []);
|
||||
|
||||
const selectedCase = useMemo(() => cases.find((c) => c.id === caseId), [cases, caseId]);
|
||||
|
||||
const submit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!caseId) { toast.error("Select a case"); return; }
|
||||
if (!title.trim() || !body.trim()) { toast.error("Date/time and details required"); return; }
|
||||
setSubmitting(true);
|
||||
const { error } = await supabase.from("status_updates").insert({
|
||||
case_id: caseId,
|
||||
title: title.trim(),
|
||||
body: body.trim(),
|
||||
created_by: user?.id,
|
||||
});
|
||||
setSubmitting(false);
|
||||
if (error) { toast.error(error.message); return; }
|
||||
toast.success("Update logged");
|
||||
setBody("");
|
||||
setTitle(nowLocal());
|
||||
loadRecent();
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="Status Updates"
|
||||
description="Quickly log a status update against any case without leaving this page."
|
||||
/>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.2fr)]">
|
||||
{/* Form */}
|
||||
<Card className="border-border/60">
|
||||
<CardContent className="p-5">
|
||||
<form onSubmit={submit} className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label>Case</Label>
|
||||
<Select value={caseId} onValueChange={setCaseId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a case…" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{cases.map((c) => (
|
||||
<SelectItem key={c.id} value={c.id}>
|
||||
{c.case_number} — {c.title}
|
||||
{c.client?.name ? ` · ${c.client.name}` : ""}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedCase && (
|
||||
<Link
|
||||
to="/cases/$caseId"
|
||||
params={{ caseId: selectedCase.id }}
|
||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-primary mt-1"
|
||||
>
|
||||
Open case <ExternalLink className="h-3 w-3" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Date & time</Label>
|
||||
<Input type="datetime-local" value={title} onChange={(e) => setTitle(e.target.value)} required />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Details</Label>
|
||||
<Textarea
|
||||
rows={6}
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
required
|
||||
maxLength={5000}
|
||||
placeholder="What happened?"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={submitting || !caseId}>
|
||||
{submitting ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Send className="h-4 w-4 mr-2" />}
|
||||
Log update
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recent */}
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs uppercase tracking-wider text-muted-foreground px-1">
|
||||
Recent updates (last 25)
|
||||
</div>
|
||||
{loading && (
|
||||
<Card className="border-border/60"><CardContent className="p-6 text-center text-sm text-muted-foreground">Loading…</CardContent></Card>
|
||||
)}
|
||||
{!loading && recent.length === 0 && (
|
||||
<Card className="border-border/60"><CardContent className="p-6 text-center text-sm text-muted-foreground">No updates yet.</CardContent></Card>
|
||||
)}
|
||||
{recent.map((u) => (
|
||||
<Card key={u.id} className="border-border/60">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between gap-3 mb-1">
|
||||
<div className="min-w-0">
|
||||
<div className="font-serif text-base">{formatDateTime(u.title)}</div>
|
||||
<div className="text-xs text-muted-foreground truncate">
|
||||
{u.case ? (
|
||||
<Link to="/cases/$caseId" params={{ caseId: u.case.id }} className="hover:text-primary">
|
||||
{u.case.case_number} — {u.case.title}
|
||||
{u.case.client?.name ? ` · ${u.case.client.name}` : ""}
|
||||
</Link>
|
||||
) : "—"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground whitespace-nowrap">
|
||||
{u.user?.full_name || u.user?.email || "Unknown"} · {formatDateTime(u.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm whitespace-pre-wrap mt-2">{u.body}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user