fix: add array guards to all frontend components consuming API responses
This commit is contained in:
@@ -27,7 +27,7 @@ export default function AIExtractPage() {
|
|||||||
queryFn: () => api.get<PaginatedResponse<Case>>("/cases"),
|
queryFn: () => api.get<PaginatedResponse<Case>>("/cases"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const cases = casesData?.data ?? [];
|
const cases = Array.isArray(casesData?.data) ? casesData.data : [];
|
||||||
|
|
||||||
async function handleExtract(file: File | null, text: string) {
|
async function handleExtract(file: File | null, text: string) {
|
||||||
setIsExtracting(true);
|
setIsExtracting(true);
|
||||||
|
|||||||
@@ -132,8 +132,8 @@ export default function CaseDetailPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const deadlines = deadlinesData?.deadlines ?? [];
|
const deadlines = Array.isArray(deadlinesData?.deadlines) ? deadlinesData.deadlines : [];
|
||||||
const documents = documentsData ?? [];
|
const documents = Array.isArray(documentsData) ? documentsData : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
@@ -205,7 +205,7 @@ export default function CaseDetailPage() {
|
|||||||
{caseDetail.deadlines_count}
|
{caseDetail.deadlines_count}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{tab.key === "parties" && caseDetail.parties.length > 0 && (
|
{tab.key === "parties" && Array.isArray(caseDetail.parties) && caseDetail.parties.length > 0 && (
|
||||||
<span className="ml-1 rounded-full bg-neutral-100 px-1.5 py-0.5 text-xs text-neutral-500">
|
<span className="ml-1 rounded-full bg-neutral-100 px-1.5 py-0.5 text-xs text-neutral-500">
|
||||||
{caseDetail.parties.length}
|
{caseDetail.parties.length}
|
||||||
</span>
|
</span>
|
||||||
@@ -217,7 +217,7 @@ export default function CaseDetailPage() {
|
|||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
{activeTab === "timeline" && (
|
{activeTab === "timeline" && (
|
||||||
<CaseTimeline events={caseDetail.recent_events ?? []} />
|
<CaseTimeline events={Array.isArray(caseDetail.recent_events) ? caseDetail.recent_events : []} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "deadlines" && (
|
{activeTab === "deadlines" && (
|
||||||
@@ -229,7 +229,7 @@ export default function CaseDetailPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "parties" && (
|
{activeTab === "parties" && (
|
||||||
<PartyList caseId={id} parties={caseDetail.parties ?? []} />
|
<PartyList caseId={id} parties={Array.isArray(caseDetail.parties) ? caseDetail.parties : []} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default function CasesPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const cases = data?.cases ?? [];
|
const cases = Array.isArray(data?.cases) ? data.cases : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
|
|||||||
@@ -80,17 +80,17 @@ export default function DashboardPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DeadlineTrafficLights data={data.deadline_summary} />
|
<DeadlineTrafficLights data={data.deadline_summary ?? { overdue_count: 0, due_this_week: 0, due_next_week: 0, ok_count: 0 }} />
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<UpcomingTimeline
|
<UpcomingTimeline
|
||||||
deadlines={data.upcoming_deadlines}
|
deadlines={Array.isArray(data.upcoming_deadlines) ? data.upcoming_deadlines : []}
|
||||||
appointments={data.upcoming_appointments}
|
appointments={Array.isArray(data.upcoming_appointments) ? data.upcoming_appointments : []}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<CaseOverviewGrid data={data.case_summary} />
|
<CaseOverviewGrid data={data.case_summary ?? { active_count: 0, new_this_month: 0, closed_count: 0 }} />
|
||||||
<AISummaryCard data={data} />
|
<AISummaryCard data={data} />
|
||||||
<QuickActions />
|
<QuickActions />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function FristenPage() {
|
|||||||
{view === "list" ? (
|
{view === "list" ? (
|
||||||
<DeadlineList />
|
<DeadlineList />
|
||||||
) : (
|
) : (
|
||||||
<DeadlineCalendarView deadlines={deadlines || []} />
|
<DeadlineCalendarView deadlines={Array.isArray(deadlines) ? deadlines : []} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export default function TerminePage() {
|
|||||||
<AppointmentList onEdit={handleEdit} />
|
<AppointmentList onEdit={handleEdit} />
|
||||||
) : (
|
) : (
|
||||||
<AppointmentCalendar
|
<AppointmentCalendar
|
||||||
appointments={appointments || []}
|
appointments={Array.isArray(appointments) ? appointments : []}
|
||||||
onAppointmentClick={handleEdit}
|
onAppointmentClick={handleEdit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -73,12 +73,13 @@ export function AppointmentList({ onEdit }: AppointmentListProps) {
|
|||||||
|
|
||||||
const caseMap = useMemo(() => {
|
const caseMap = useMemo(() => {
|
||||||
const map = new Map<string, Case>();
|
const map = new Map<string, Case>();
|
||||||
cases?.cases?.forEach((c) => map.set(c.id, c));
|
const arr = Array.isArray(cases?.cases) ? cases.cases : [];
|
||||||
|
arr.forEach((c) => map.set(c.id, c));
|
||||||
return map;
|
return map;
|
||||||
}, [cases]);
|
}, [cases]);
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
if (!appointments) return [];
|
if (!Array.isArray(appointments)) return [];
|
||||||
return appointments
|
return appointments
|
||||||
.filter((a) => {
|
.filter((a) => {
|
||||||
if (caseFilter !== "all" && a.case_id !== caseFilter) return false;
|
if (caseFilter !== "all" && a.case_id !== caseFilter) return false;
|
||||||
@@ -91,7 +92,7 @@ export function AppointmentList({ onEdit }: AppointmentListProps) {
|
|||||||
const grouped = useMemo(() => groupByDate(filtered), [filtered]);
|
const grouped = useMemo(() => groupByDate(filtered), [filtered]);
|
||||||
|
|
||||||
const counts = useMemo(() => {
|
const counts = useMemo(() => {
|
||||||
if (!appointments) return { today: 0, thisWeek: 0, total: 0 };
|
if (!Array.isArray(appointments)) return { today: 0, thisWeek: 0, total: 0 };
|
||||||
let today = 0;
|
let today = 0;
|
||||||
let thisWeek = 0;
|
let thisWeek = 0;
|
||||||
for (const a of appointments) {
|
for (const a of appointments) {
|
||||||
@@ -148,7 +149,7 @@ export function AppointmentList({ onEdit }: AppointmentListProps) {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{cases?.cases && cases.cases.length > 0 && (
|
{Array.isArray(cases?.cases) && cases.cases.length > 0 && (
|
||||||
<select
|
<select
|
||||||
value={caseFilter}
|
value={caseFilter}
|
||||||
onChange={(e) => setCaseFilter(e.target.value)}
|
onChange={(e) => setCaseFilter(e.target.value)}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ interface Props {
|
|||||||
|
|
||||||
function generateSummary(data: DashboardData): string {
|
function generateSummary(data: DashboardData): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
const { deadline_summary: ds, case_summary: cs, upcoming_deadlines: ud } = data;
|
const ds = data.deadline_summary ?? { overdue_count: 0, due_this_week: 0, due_next_week: 0, ok_count: 0 };
|
||||||
|
const cs = data.case_summary ?? { active_count: 0, new_this_month: 0, closed_count: 0 };
|
||||||
|
const ud = Array.isArray(data.upcoming_deadlines) ? data.upcoming_deadlines : [];
|
||||||
|
|
||||||
// Deadline urgency
|
// Deadline urgency
|
||||||
if (ds.overdue_count > 0) {
|
if (ds.overdue_count > 0) {
|
||||||
|
|||||||
@@ -8,24 +8,25 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CaseOverviewGrid({ data }: Props) {
|
export function CaseOverviewGrid({ data }: Props) {
|
||||||
|
const safe = data ?? { active_count: 0, new_this_month: 0, closed_count: 0 };
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
label: "Aktive Akten",
|
label: "Aktive Akten",
|
||||||
value: data.active_count,
|
value: safe.active_count ?? 0,
|
||||||
icon: FolderOpen,
|
icon: FolderOpen,
|
||||||
color: "text-blue-600",
|
color: "text-blue-600",
|
||||||
bg: "bg-blue-50",
|
bg: "bg-blue-50",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Neu (Monat)",
|
label: "Neu (Monat)",
|
||||||
value: data.new_this_month,
|
value: safe.new_this_month ?? 0,
|
||||||
icon: FolderPlus,
|
icon: FolderPlus,
|
||||||
color: "text-violet-600",
|
color: "text-violet-600",
|
||||||
bg: "bg-violet-50",
|
bg: "bg-violet-50",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Abgeschlossen",
|
label: "Abgeschlossen",
|
||||||
value: data.closed_count,
|
value: safe.closed_count ?? 0,
|
||||||
icon: Archive,
|
icon: Archive,
|
||||||
color: "text-neutral-500",
|
color: "text-neutral-500",
|
||||||
bg: "bg-neutral-50",
|
bg: "bg-neutral-50",
|
||||||
|
|||||||
@@ -31,24 +31,25 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DeadlineTrafficLights({ data, onFilter }: Props) {
|
export function DeadlineTrafficLights({ data, onFilter }: Props) {
|
||||||
|
const safe = data ?? { overdue_count: 0, due_this_week: 0, due_next_week: 0, ok_count: 0 };
|
||||||
const cards = [
|
const cards = [
|
||||||
{
|
{
|
||||||
key: "overdue" as const,
|
key: "overdue" as const,
|
||||||
label: "Überfällig",
|
label: "Überfällig",
|
||||||
count: data.overdue_count,
|
count: safe.overdue_count ?? 0,
|
||||||
icon: AlertTriangle,
|
icon: AlertTriangle,
|
||||||
bg: "bg-red-50",
|
bg: "bg-red-50",
|
||||||
border: "border-red-200",
|
border: "border-red-200",
|
||||||
iconColor: "text-red-500",
|
iconColor: "text-red-500",
|
||||||
countColor: "text-red-700",
|
countColor: "text-red-700",
|
||||||
labelColor: "text-red-600",
|
labelColor: "text-red-600",
|
||||||
ring: data.overdue_count > 0 ? "ring-2 ring-red-300 ring-offset-1" : "",
|
ring: (safe.overdue_count ?? 0) > 0 ? "ring-2 ring-red-300 ring-offset-1" : "",
|
||||||
pulse: data.overdue_count > 0,
|
pulse: (safe.overdue_count ?? 0) > 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "this_week" as const,
|
key: "this_week" as const,
|
||||||
label: "Diese Woche",
|
label: "Diese Woche",
|
||||||
count: data.due_this_week,
|
count: safe.due_this_week ?? 0,
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
bg: "bg-amber-50",
|
bg: "bg-amber-50",
|
||||||
border: "border-amber-200",
|
border: "border-amber-200",
|
||||||
@@ -61,7 +62,7 @@ export function DeadlineTrafficLights({ data, onFilter }: Props) {
|
|||||||
{
|
{
|
||||||
key: "ok" as const,
|
key: "ok" as const,
|
||||||
label: "Im Zeitplan",
|
label: "Im Zeitplan",
|
||||||
count: data.ok_count + data.due_next_week,
|
count: (safe.ok_count ?? 0) + (safe.due_next_week ?? 0),
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
bg: "bg-emerald-50",
|
bg: "bg-emerald-50",
|
||||||
border: "border-emerald-200",
|
border: "border-emerald-200",
|
||||||
|
|||||||
@@ -21,13 +21,16 @@ function formatDayLabel(date: Date): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function UpcomingTimeline({ deadlines, appointments }: Props) {
|
export function UpcomingTimeline({ deadlines, appointments }: Props) {
|
||||||
|
const safeDeadlines = Array.isArray(deadlines) ? deadlines : [];
|
||||||
|
const safeAppointments = Array.isArray(appointments) ? appointments : [];
|
||||||
|
|
||||||
const items: TimelineItem[] = [
|
const items: TimelineItem[] = [
|
||||||
...deadlines.map((d) => ({
|
...safeDeadlines.map((d) => ({
|
||||||
type: "deadline" as const,
|
type: "deadline" as const,
|
||||||
date: parseISO(d.due_date),
|
date: parseISO(d.due_date),
|
||||||
data: d,
|
data: d,
|
||||||
})),
|
})),
|
||||||
...appointments.map((a) => ({
|
...safeAppointments.map((a) => ({
|
||||||
type: "appointment" as const,
|
type: "appointment" as const,
|
||||||
date: parseISO(a.start_at),
|
date: parseISO(a.start_at),
|
||||||
data: a,
|
data: a,
|
||||||
|
|||||||
@@ -76,12 +76,12 @@ export function DeadlineList() {
|
|||||||
|
|
||||||
const caseMap = useMemo(() => {
|
const caseMap = useMemo(() => {
|
||||||
const map = new Map<string, Case>();
|
const map = new Map<string, Case>();
|
||||||
cases?.forEach((c) => map.set(c.id, c));
|
(Array.isArray(cases) ? cases : []).forEach((c) => map.set(c.id, c));
|
||||||
return map;
|
return map;
|
||||||
}, [cases]);
|
}, [cases]);
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
if (!deadlines) return [];
|
if (!Array.isArray(deadlines)) return [];
|
||||||
return deadlines.filter((d) => {
|
return deadlines.filter((d) => {
|
||||||
if (statusFilter === "pending" && d.status !== "pending") return false;
|
if (statusFilter === "pending" && d.status !== "pending") return false;
|
||||||
if (statusFilter === "completed" && d.status !== "completed")
|
if (statusFilter === "completed" && d.status !== "completed")
|
||||||
@@ -96,7 +96,7 @@ export function DeadlineList() {
|
|||||||
}, [deadlines, statusFilter, caseFilter]);
|
}, [deadlines, statusFilter, caseFilter]);
|
||||||
|
|
||||||
const counts = useMemo(() => {
|
const counts = useMemo(() => {
|
||||||
if (!deadlines) return { overdue: 0, thisWeek: 0, ok: 0 };
|
if (!Array.isArray(deadlines)) return { overdue: 0, thisWeek: 0, ok: 0 };
|
||||||
let overdue = 0,
|
let overdue = 0,
|
||||||
thisWeek = 0,
|
thisWeek = 0,
|
||||||
ok = 0;
|
ok = 0;
|
||||||
@@ -188,7 +188,7 @@ export function DeadlineList() {
|
|||||||
<option value="completed">Erledigt</option>
|
<option value="completed">Erledigt</option>
|
||||||
<option value="overdue">Überfällig</option>
|
<option value="overdue">Überfällig</option>
|
||||||
</select>
|
</select>
|
||||||
{cases && cases.length > 0 && (
|
{Array.isArray(cases) && cases.length > 0 && (
|
||||||
<select
|
<select
|
||||||
value={caseFilter}
|
value={caseFilter}
|
||||||
onChange={(e) => setCaseFilter(e.target.value)}
|
onChange={(e) => setCaseFilter(e.target.value)}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export function TenantSwitcher() {
|
|||||||
api
|
api
|
||||||
.get<TenantWithRole[]>("/tenants")
|
.get<TenantWithRole[]>("/tenants")
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
if (!Array.isArray(data)) return;
|
||||||
setTenants(data);
|
setTenants(data);
|
||||||
const savedId = localStorage.getItem("kanzlai_tenant_id");
|
const savedId = localStorage.getItem("kanzlai_tenant_id");
|
||||||
const match = data.find((t) => t.id === savedId) || data[0];
|
const match = data.find((t) => t.id === savedId) || data[0];
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export function TeamSettings() {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Members List */}
|
{/* Members List */}
|
||||||
{members && members.length > 0 ? (
|
{Array.isArray(members) && members.length > 0 ? (
|
||||||
<div className="overflow-hidden rounded-md border border-neutral-200">
|
<div className="overflow-hidden rounded-md border border-neutral-200">
|
||||||
{members.map((member, i) => {
|
{members.map((member, i) => {
|
||||||
const roleInfo = ROLE_LABELS[member.role] || ROLE_LABELS.member;
|
const roleInfo = ROLE_LABELS[member.role] || ROLE_LABELS.member;
|
||||||
|
|||||||
Reference in New Issue
Block a user