154 lines
5.0 KiB
TypeScript
154 lines
5.0 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import useAuthStore from '../../../store/authStore';
|
|
|
|
export type AdminInvoice = {
|
|
id: string | number;
|
|
invoice_number?: string | null;
|
|
user_id?: string | number | null;
|
|
buyer_name?: string | null;
|
|
buyer_email?: string | null;
|
|
buyer_street?: string | null;
|
|
buyer_postal_code?: string | null;
|
|
buyer_city?: string | null;
|
|
buyer_country?: string | null;
|
|
currency?: string | null;
|
|
total_net?: number | null;
|
|
total_tax?: number | null;
|
|
total_gross?: number | null;
|
|
vat_rate?: number | null;
|
|
status?: string;
|
|
issued_at?: string | null;
|
|
due_at?: string | null;
|
|
pdf_storage_key?: string | null;
|
|
context?: any | null;
|
|
created_at?: string | null;
|
|
updated_at?: string | null;
|
|
};
|
|
|
|
export type AdminInvoiceRevenueSummary = {
|
|
totalPaidAllTime: number;
|
|
currency?: string | null;
|
|
paidInvoiceCount?: number;
|
|
};
|
|
|
|
export function useAdminInvoices(params?: { status?: string; limit?: number; offset?: number }) {
|
|
const accessToken = useAuthStore(s => s.accessToken);
|
|
const [invoices, setInvoices] = useState<AdminInvoice[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string>('');
|
|
const inFlight = useRef<AbortController | null>(null);
|
|
|
|
const fetchInvoices = useCallback(async () => {
|
|
setError('');
|
|
// Abort previous
|
|
inFlight.current?.abort();
|
|
const controller = new AbortController();
|
|
inFlight.current = controller;
|
|
|
|
try {
|
|
const base = process.env.NEXT_PUBLIC_API_BASE_URL || '';
|
|
const qp = new URLSearchParams();
|
|
if (params?.status) qp.set('status', params.status);
|
|
qp.set('limit', String(params?.limit ?? 200));
|
|
qp.set('offset', String(params?.offset ?? 0));
|
|
const url = `${base}/api/admin/invoices${qp.toString() ? `?${qp.toString()}` : ''}`;
|
|
|
|
setLoading(true);
|
|
const res = await fetch(url, {
|
|
method: 'GET',
|
|
credentials: 'include',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
|
},
|
|
signal: controller.signal,
|
|
});
|
|
|
|
const body = await res.json().catch(() => ({}));
|
|
if (!res.ok || body?.success === false) {
|
|
setInvoices([]);
|
|
setError(body?.message || `Failed to load invoices (${res.status})`);
|
|
return;
|
|
}
|
|
const list: AdminInvoice[] = Array.isArray(body?.data) ? body.data : [];
|
|
// sort fallback (issued_at DESC then created_at DESC)
|
|
list.sort((a, b) => {
|
|
const ad = new Date(a.issued_at ?? a.created_at ?? 0).getTime();
|
|
const bd = new Date(b.issued_at ?? b.created_at ?? 0).getTime();
|
|
return bd - ad;
|
|
});
|
|
setInvoices(list);
|
|
} catch (e: any) {
|
|
if (e?.name === 'AbortError') return;
|
|
setError(e?.message || 'Network error');
|
|
setInvoices([]);
|
|
} finally {
|
|
setLoading(false);
|
|
if (inFlight.current === controller) inFlight.current = null;
|
|
}
|
|
}, [accessToken, params?.status, params?.limit, params?.offset]);
|
|
|
|
useEffect(() => {
|
|
if (accessToken) fetchInvoices();
|
|
return () => inFlight.current?.abort();
|
|
}, [accessToken, fetchInvoices]);
|
|
|
|
return { invoices, loading, error, reload: fetchInvoices };
|
|
}
|
|
|
|
export function useAdminInvoiceRevenueSummary() {
|
|
const accessToken = useAuthStore(s => s.accessToken);
|
|
const [summary, setSummary] = useState<AdminInvoiceRevenueSummary | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string>('');
|
|
const inFlight = useRef<AbortController | null>(null);
|
|
|
|
const fetchSummary = useCallback(async () => {
|
|
setError('');
|
|
inFlight.current?.abort();
|
|
const controller = new AbortController();
|
|
inFlight.current = controller;
|
|
|
|
try {
|
|
const base = process.env.NEXT_PUBLIC_API_BASE_URL || '';
|
|
const url = `${base}/api/admin/invoices/revenue-summary`;
|
|
|
|
setLoading(true);
|
|
const res = await fetch(url, {
|
|
method: 'GET',
|
|
credentials: 'include',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
|
},
|
|
signal: controller.signal,
|
|
});
|
|
|
|
const body = await res.json().catch(() => ({}));
|
|
if (!res.ok || body?.success === false) {
|
|
setSummary(null);
|
|
setError(body?.message || `Failed to load revenue summary (${res.status})`);
|
|
return;
|
|
}
|
|
|
|
setSummary(body?.data || { totalPaidAllTime: 0, currency: 'EUR', paidInvoiceCount: 0 });
|
|
} catch (e: any) {
|
|
if (e?.name === 'AbortError') return;
|
|
setError(e?.message || 'Network error');
|
|
setSummary(null);
|
|
} finally {
|
|
setLoading(false);
|
|
if (inFlight.current === controller) inFlight.current = null;
|
|
}
|
|
}, [accessToken]);
|
|
|
|
useEffect(() => {
|
|
if (accessToken) fetchSummary();
|
|
return () => inFlight.current?.abort();
|
|
}, [accessToken, fetchSummary]);
|
|
|
|
return { summary, loading, error, reload: fetchSummary };
|
|
}
|