import { useMemo, useState } from 'react'; import { Link, useParams } from 'react-router-dom'; import { Badge } from '@astryxdesign/core/Badge'; import { Button } from '@astryxdesign/core/Button'; import { Card } from '@astryxdesign/core/Card'; import { HStack } from '@astryxdesign/core/HStack'; import { Table, type TableColumn } from '@astryxdesign/core/Table'; import { Text } from '@astryxdesign/core/Text'; import { VStack } from '@astryxdesign/core/VStack'; import { IndianRupee, QrCode, ShoppingCart, Store, TriangleAlert, Truck } from 'lucide-react'; import { DataState } from '@/components/DataState'; import { Freshness } from '@/components/Freshness'; import { KpiCard } from '@/components/KpiCard'; import { PageHeader } from '@/components/PageHeader'; import { SectionHeader } from '@/components/SectionHeader'; import { useOrderSummary, useOrders, useTenantLocations, useTenants } from '@/queries/hooks'; import { branchOrderStats, NO_ORDERS } from '@/features/store-admin/branchStats'; import type { TenantInfo, TenantLocation } from '@/api/types'; import { TablePager } from '@/components/TablePager'; import { usePaged } from '@/components/usePaged'; import { Drawer } from '@/features/store-admin/Drawer'; import { StoreQrPanel } from '@/features/qr/StoreQrPanel'; import { PartnerAssignDrawer } from '../PartnerAssignDrawer'; interface BranchRow extends Record { locationid: number; locationname: string; city: string; hours: string; radius: string; status: string; orders: number; revenue: number; } function money(value: number | undefined): string { if (typeof value !== 'number' || Number.isNaN(value)) return '₹0'; return `₹${value.toLocaleString('en-IN')}`; } /** * One tenant: its branches, and how each is performing. * * Order figures are totalled from the tenant's own order rows and grouped by * branch. They used to come from `/orders/getlocationsummary`, which does return * one row per branch but carries no money on it at all — so every Revenue cell * in this table read ₹0 for as long as the page has existed. * * Counter sales are NOT folded in here: the POS endpoints take a single required * locationid, so a tenant-wide till figure would mean one request per branch, * and a blended number would be eventually consistent in a way this page cannot * honestly caption. Branch-level POS lives on the branch screen instead. */ export function StoreDetailPage() { const { tenantId } = useParams<{ tenantId: string }>(); const tenantid = Number(tenantId ?? 0) || undefined; const { data: tenants } = useTenants(); const { data: locations, isLoading, error } = useTenantLocations(tenantid); /* Per-branch takings come from the order rows. `getlocationsummary`, which used to feed this table, sends counts and no money at all — see `branchStats.ts`. No date range here on purpose: this is the platform's all-time view of a merchant, not a trading-day board. */ const summary = useOrders(tenantid ? { tenantid, pagesize: 500 } : undefined); const orders = useOrderSummary(tenantid); const tenant = useMemo( () => (tenants as TenantInfo[] | undefined)?.find((entry) => entry.tenantid === tenantid), [tenants, tenantid], ); const rows = useMemo(() => { const perLocation = branchOrderStats(summary.data ?? []); return ((locations ?? []) as TenantLocation[]).map((branch) => { const stats = perLocation.get(branch.locationid) ?? NO_ORDERS; return { locationid: branch.locationid, locationname: branch.locationname, city: [branch.suburb, branch.city].filter(Boolean).join(', '), hours: branch.opentime && branch.closetime ? `${branch.opentime}–${branch.closetime}` : '—', radius: branch.deliveryradius ? `${(branch.deliveryradius / 1000).toFixed(1)} km` : '—', status: branch.status ?? 'Unknown', // Both channels, because this page answers "how is this merchant // doing" rather than "through which door". `branchOrderStats` splits // app orders from imported counter bills — a split the Store Admin // console needs and this page does not — so they are added back here // rather than left half-counted. orders: stats.orders + stats.counterOrders, revenue: stats.revenue + stats.counterRevenue, }; }); }, [locations, summary.data]); const paged = usePaged(rows); const totals = useMemo(() => { const branches = rows.length; const active = rows.filter((row) => row.status.toLowerCase() === 'active').length; const totalOrders = rows.reduce((sum, row) => sum + row.orders, 0); const totalRevenue = rows.reduce((sum, row) => sum + row.revenue, 0); const cancelled = Number(orders.data?.cancelled ?? 0); return { branches, active, totalOrders, totalRevenue, cancelled }; }, [rows, orders.data]); /** The branch whose code is on screen, if any. */ const [qrFor, setQrFor] = useState(null); /** Open while the merchant's delivery partner is being changed. */ const [isPartnerOpen, setPartnerOpen] = useState(false); const columns: TableColumn[] = [ { key: 'locationname', header: 'Branch', width: { type: 'proportional', value: 3 }, renderCell: (row) => ( {row.locationname} {row.city || '—'} ), }, { key: 'hours', header: 'Hours', width: { type: 'pixel', value: 130 }, renderCell: (row) => ( {row.hours} ), }, { key: 'radius', header: 'Radius', align: 'end', width: { type: 'pixel', value: 90 }, renderCell: (row) => ( {row.radius} ), }, { key: 'orders', header: 'Orders', align: 'end', width: { type: 'pixel', value: 100 }, renderCell: (row) => ( {row.orders} ), }, { key: 'revenue', header: 'Revenue', align: 'end', width: { type: 'pixel', value: 120 }, renderCell: (row) => ( {money(row.revenue)} ), }, { key: 'status', header: 'Status', align: 'end', width: { type: 'pixel', value: 110 }, renderCell: (row) => ( ), }, { /* The storefront code, per branch, after the day it was commissioned. Shown at creation too — but a poster gets lost, a shop reopens, a merchant asks for it again a month later, and the person they ask is whoever is looking at this page. The code is derived from the two ids on this row, so there is nothing to look up and nothing to reissue. */ key: 'qr', header: 'QR', align: 'end', width: { type: 'pixel', value: 84 }, renderCell: (row) => (