import React, { useEffect, useMemo, useState } from 'react';
import { useInfiniteQuery } from '@tanstack/react-query';
import { MdCalendarMonth } from 'react-icons/md';
import { Card } from '@astryxdesign/core/Card';
import { HStack } from '@astryxdesign/core/HStack';
import { VStack } from '@astryxdesign/core/VStack';
import { EmptyState } from '@astryxdesign/core/EmptyState';
import { DateInput } from '@astryxdesign/core/DateInput';
import dayjs from 'dayjs';
import { parseDoormileTimestamp } from 'utils/doormileTimestamp';
import { fetchDeliveries } from 'pages/api/api';
import Loader from 'components/Loader';
import PageHeader from 'components/nearle_components/PageHeader';
import ProfitabilitySection, {
BASE_REVENUE,
BASE_KM_LIMIT,
EXTRA_RATE_KM,
VARIABLE_RATE_KM,
FIXED_COST_PER_SLOT,
BATCHES_DEFAULT
} from '../dispatch/ProfitabilitySection';
import { SKIPPED_STATUSES } from '../dispatch/dispatchShared';
// ============================================================================
// Standalone Profitability report — reference project (nearle_console_express)
// has this as its own page (reports/profitability.js); here it only existed
// embedded inside Dispatch.js's live map. Rather than rebuild the UI (the
// reference re-implements the profit math inline in raw MUI), this reuses
// the already-built, already-tested ProfitabilitySection.js component as-is
// and just supplies it with independently-fetched data — mirrors how the
// reference page ALSO re-fetches and re-groups independently rather than
// reusing its own Dispatch page's pipeline (which is entangled with
// Leaflet/GPS/zone-bucketing state not needed for a report).
//
// GET /admin/bookings has no server-side date/zone/status filter params (see
// src/pages/api/CLAUDE.md) — bulk-fetched and grouped entirely client-side,
// same pattern as reports/ordersDetails.js and deliveries.js.
// ============================================================================
const BRAND = '#000000';
// Profit formulas + batch windows imported from ProfitabilitySection.js
// (the single source of truth) rather than duplicated here — used to total
// across all riders for the daily KPI chip; per-rider numbers are computed
// by ProfitabilitySection itself from the same `riders` data, so this must
// match exactly or the chip and the table below it would disagree.
const orderRevenue = (km) => (km <= BASE_KM_LIMIT ? BASE_REVENUE : BASE_REVENUE + (km - BASE_KM_LIMIT) * EXTRA_RATE_KM);
// Buckets on `orderdate` (the booking's createdat) and parses with
// parseDoormileTimestamp — must match ProfitabilitySection.getRowBatch,
// Dispatch.js's BATCH_TIME_FIELD and deliveries.js's BATCH_TIME_KEYS.
//
// It used to read `assigntime`, which api.js maps to the backend's
// last-modified column, so it was re-stamped by any status change and a
// rider's slot count drifted through the day; and it parsed with a bare
// dayjs(), which mis-handles the false trailing Z some Doormile timestamps
// carry. Slot count feeds the FIXED cost, so both bugs moved money.
const getOrderBatch = (o) => {
const t = o.orderdate;
if (!t || /^\d{4}-\d{2}-\d{2}$/.test(String(t).trim())) return null;
const d = parseDoormileTimestamp(t);
if (!d.isValid()) return null;
const hour = d.hour() + d.minute() / 60;
const batch = BATCHES_DEFAULT.find((b) => hour >= b.startHour && hour < b.endHour);
return batch?.id || null;
};
const Profitability = () => {
const [selectedDate, setSelectedDate] = useState(dayjs().format('YYYY-MM-DD'));
const {
data: deliveriesData,
isLoading,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ['fetchdeliveries-profitability', 0, 0, 'all', selectedDate, selectedDate, 500, '', 0, 0, 0],
queryFn: fetchDeliveries,
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined
});
const allRows = useMemo(() => (deliveriesData?.pages || []).flatMap((page) => page.rows || []), [deliveriesData]);
useEffect(() => {
if (hasNextPage && !isFetchingNextPage) fetchNextPage();
}, [hasNextPage, isFetchingNextPage, fetchNextPage, allRows.length]);
// Group into { id, riderName, orders } — flat, no zone/suburb bucketing
// (that's a Dispatch-map-specific concern, not needed for a report).
// riderkms is what ProfitabilitySection's revenue/cost math actually reads
// (see orderRevenue/calcRiderMetrics there) but GET /admin/bookings has no
// road-distance field — mapped from the same haversine straight-line `kms`
// every other page in this app already uses as the distance proxy.
const riders = useMemo(() => {
const byRider = new Map();
allRows.forEach((row) => {
if (!row.userid) return;
const key = String(row.userid);
if (!byRider.has(key)) {
byRider.set(key, { id: key, riderName: row.ridername || `Rider #${key}`, orders: [] });
}
byRider.get(key).orders.push({ ...row, riderkms: row.kms });
});
return Array.from(byRider.values());
}, [allRows]);
const totalDailyProfit = useMemo(() => {
let profit = 0;
riders.forEach((rider) => {
let revenue = 0;
let kms = 0;
const batchesWorked = new Set();
rider.orders.forEach((o) => {
const status = String(o.orderstatus || '').toLowerCase();
if (SKIPPED_STATUSES.has(status)) return;
// parseDoormileTimestamp (not bare dayjs()) avoids the false-trailing-Z
// parse bug (utils/doormileTimestamp.js) — see ProfitabilitySection.js.
const dateStr = o.orderdate ? parseDoormileTimestamp(o.orderdate).format('YYYY-MM-DD') : null;
if (dateStr !== selectedDate) return;
const km = parseFloat(o.riderkms || 0);
revenue += orderRevenue(km);
kms += km;
const batch = getOrderBatch(o);
if (batch) batchesWorked.add(batch);
});
const variableCost = kms * VARIABLE_RATE_KM;
const fixedCost = Math.min(batchesWorked.size, 3) * FIXED_COST_PER_SLOT;
profit += revenue - variableCost - fixedCost;
});
return profit;
}, [riders, selectedDate]);
return (
<>
{isLoading && }
v && setSelectedDate(v)} />
}
/>
{!isLoading && riders.length === 0 ? (
) : (
)}
>
);
};
export default Profitability;