dummy data removed
This commit is contained in:
parent
4d229d0b94
commit
4ed7161a33
|
|
@ -24,9 +24,11 @@ import {
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { apiGet } from "@/lib/api";
|
import { apiGet } from "@/lib/api";
|
||||||
import type { Booking, DashboardPayload } from "@/lib/types";
|
|
||||||
import { formatDate, formatMoney } from "@/lib/format";
|
import { formatDate, formatMoney } from "@/lib/format";
|
||||||
import { roomDisplayName } from "@/lib/room-utils";
|
import { roomDisplayName } from "@/lib/room-utils";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import { useAuthStore } from "@/store/authStore";
|
||||||
|
import type { Booking, DashboardPayload } from "@/lib/types";
|
||||||
|
|
||||||
const tooltipStyle = {
|
const tooltipStyle = {
|
||||||
backgroundColor: "var(--navy)",
|
backgroundColor: "var(--navy)",
|
||||||
|
|
@ -35,18 +37,111 @@ const tooltipStyle = {
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type HotelSummaryResponse = {
|
||||||
|
arrivalsToday?: number;
|
||||||
|
arrivals?: number;
|
||||||
|
departuresToday?: number;
|
||||||
|
departures?: number;
|
||||||
|
unpaidHolds?: number;
|
||||||
|
revenueMonth?: string | number;
|
||||||
|
bookingsByStatus?: Record<string, number>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isDashboardPayload(d: unknown): d is DashboardPayload {
|
||||||
|
return typeof d === "object" && d !== null && "bookingSeries" in d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHotelSummary(d: unknown): d is HotelSummaryResponse {
|
||||||
|
if (typeof d !== "object" || d === null) return false;
|
||||||
|
const x = d as HotelSummaryResponse;
|
||||||
|
return (
|
||||||
|
typeof x.arrivalsToday === "number" ||
|
||||||
|
typeof x.arrivals === "number" ||
|
||||||
|
x.bookingsByStatus !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const [data, setData] = useState<DashboardPayload | null>(null);
|
const selectedPropertyId = useAuthStore((s) => s.selectedPropertyId);
|
||||||
|
const [data, setData] = useState<DashboardPayload | HotelSummaryResponse | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
const [err, setErr] = useState<string | null>(null);
|
const [err, setErr] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
apiGet<DashboardPayload>("/dashboard")
|
setErr(null);
|
||||||
|
setData(null);
|
||||||
|
apiGet<DashboardPayload | HotelSummaryResponse>(`/dashboard/summary`)
|
||||||
.then(setData)
|
.then(setData)
|
||||||
.catch((e) => setErr(String(e)));
|
.catch((e) => setErr(String(e)));
|
||||||
}, []);
|
}, [selectedPropertyId]);
|
||||||
|
|
||||||
if (err) return <p className="text-destructive">{err}</p>;
|
if (err) return <p className="text-destructive">{err}</p>;
|
||||||
if (!data) return <p className="text-muted-foreground">Loading…</p>;
|
if (!data) return (
|
||||||
|
<div className="flex min-h-[400px] items-center justify-center">
|
||||||
|
<Spinner size={32} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isHotelSummary(data) && !isDashboardPayload(data)) {
|
||||||
|
const arrivals = data.arrivalsToday ?? data.arrivals ?? 0;
|
||||||
|
const departures = data.departuresToday ?? data.departures ?? 0;
|
||||||
|
const revenueRaw = data.revenueMonth ?? 0;
|
||||||
|
const revenueNum =
|
||||||
|
typeof revenueRaw === "string" ? parseFloat(revenueRaw) : Number(revenueRaw);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight">Dashboard</h1>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{[
|
||||||
|
{ label: "Arrivals today", value: arrivals },
|
||||||
|
{ label: "Departures today", value: departures },
|
||||||
|
{ label: "Unpaid holds", value: data.unpaidHolds ?? 0 },
|
||||||
|
{
|
||||||
|
label: "Revenue (month)",
|
||||||
|
value: formatMoney(Number.isFinite(revenueNum) ? revenueNum : 0),
|
||||||
|
},
|
||||||
|
].map((c) => (
|
||||||
|
<Card key={c.label} className="rounded-2xl">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
{c.label}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-2xl font-bold">{c.value}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{data.bookingsByStatus && Object.keys(data.bookingsByStatus).length > 0 ? (
|
||||||
|
<Card className="rounded-2xl">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Bookings by status</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-wrap gap-2">
|
||||||
|
{Object.entries(data.bookingsByStatus).map(([k, v]) => (
|
||||||
|
<Badge key={k} variant="secondary">
|
||||||
|
{k}: {v}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDashboardPayload(data)) {
|
||||||
|
return (
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Unexpected dashboard response. Try again or check the API.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
@ -263,7 +358,7 @@ export function DashboardPage() {
|
||||||
{formatDate(b.checkIn)} → {formatDate(b.checkOut)}
|
{formatDate(b.checkIn)} → {formatDate(b.checkOut)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-xs">
|
<TableCell className="text-xs">
|
||||||
{roomDisplayName(b.roomId)}
|
{b.roomDisplayLabel ?? roomDisplayName(b.roomId)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="secondary">{b.status}</Badge>
|
<Badge variant="secondary">{b.status}</Badge>
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,17 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { apiGet } from "@/lib/api";
|
import { apiGet } from "@/lib/api";
|
||||||
|
import { useAuthStore } from "@/store/authStore";
|
||||||
import type { Payment } from "@/lib/types";
|
import type { Payment } from "@/lib/types";
|
||||||
import { formatDateTime, formatMoney } from "@/lib/format";
|
import { formatDateTime, formatMoney } from "@/lib/format";
|
||||||
|
|
||||||
export function PaymentsPage() {
|
export function PaymentsPage() {
|
||||||
|
const selectedPropertyId = useAuthStore((s) => s.selectedPropertyId);
|
||||||
const [rows, setRows] = useState<Payment[]>([]);
|
const [rows, setRows] = useState<Payment[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
apiGet<{ data: Payment[] }>("/payments").then((r) => setRows(r.data));
|
apiGet<{ data: Payment[] }>("/payments").then((r) => setRows(r.data));
|
||||||
}, []);
|
}, [selectedPropertyId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,19 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { apiGet } from "@/lib/api";
|
import { apiGet } from "@/lib/api";
|
||||||
|
import { useAuthStore } from "@/store/authStore";
|
||||||
import type { Transaction } from "@/lib/types";
|
import type { Transaction } from "@/lib/types";
|
||||||
import { formatDateTime, formatMoney } from "@/lib/format";
|
import { formatDateTime, formatMoney } from "@/lib/format";
|
||||||
|
|
||||||
export function TransactionsPage() {
|
export function TransactionsPage() {
|
||||||
|
const selectedPropertyId = useAuthStore((s) => s.selectedPropertyId);
|
||||||
const [rows, setRows] = useState<Transaction[]>([]);
|
const [rows, setRows] = useState<Transaction[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
apiGet<{ data: Transaction[] }>("/transactions").then((r) =>
|
apiGet<{ data: Transaction[] }>("/transactions").then((r) =>
|
||||||
setRows(r.data)
|
setRows(r.data)
|
||||||
);
|
);
|
||||||
}, []);
|
}, [selectedPropertyId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user