feat: relocate orders and deliveries to store console & polish store cover images

This commit is contained in:
Suriya
2026-06-03 18:20:43 +05:30
commit 6eaeb5c4a7
32 changed files with 13430 additions and 0 deletions

View File

@@ -0,0 +1,286 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React from 'react';
import {
ShoppingBag,
PackageCheck,
Wallet,
TrendingUp,
Store,
MapPin,
Phone,
Sparkles,
AlertTriangle,
} from 'lucide-react';
import { useOrderSummary, useTenantInfo, useTenantLocations, useInvoiceInsight } from '../services/queries';
import { DEFAULT_TENANT_ID, DEFAULT_CONFIG_ID } from '../services/api';
import { useFiestaLocationSummary } from '../services/fiestaQueries';
import { FIESTA_TENANT_ID } from '../services/fiestaApi';
interface DashboardViewProps {
searchQuery: string;
isCoimbatoreView: boolean;
}
const ymd = (d: Date) =>
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
const str = (v: unknown): string => (v == null ? '' : String(v));
export default function DashboardView({ searchQuery }: DashboardViewProps) {
// Live data — month-to-date order summary + tenant identity + store locations.
const today = new Date();
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
const fromdate = ymd(monthStart);
const todate = ymd(today);
const summaryQ = useOrderSummary(DEFAULT_TENANT_ID, fromdate, todate, DEFAULT_CONFIG_ID);
const tenantQ = useTenantInfo(DEFAULT_TENANT_ID);
const locationsQ = useTenantLocations(DEFAULT_TENANT_ID);
const insightQ = useInvoiceInsight(DEFAULT_TENANT_ID);
const s = summaryQ.data;
const tenantName = str(tenantQ.data?.tenantname) || s?.tenantname || `Tenant ${DEFAULT_TENANT_ID}`;
// Profit comes from the live invoice/financial insight. When the tenant has no
// invoice records we show "—" rather than a misleading ₹0.
const insight = insightQ.data;
const money = (v: number | null) => (v == null ? '—' : `${Math.round(v).toLocaleString('en-IN')}`);
const todaysProfit = insight ? insight.profit : null;
const monthlyProfit = insight ? insight.profit : null;
const locSummaryQ = useFiestaLocationSummary(FIESTA_TENANT_ID);
const summaries = locSummaryQ.data ?? [];
const locations = (locationsQ.data ?? []).filter((loc) => {
if (!searchQuery) return true;
const q = searchQuery.toLowerCase();
return (
str(loc.locationname).toLowerCase().includes(q) ||
str(loc.city).toLowerCase().includes(q) ||
str(loc.suburb).toLowerCase().includes(q)
);
});
// KPI cards — orders from getordersummary, profit from getinvoiceinsight.
const totalStoresCount = locations.length;
const activeStoresCount = locations.filter(l => str(l.status).toLowerCase() === 'active').length;
const inactiveStoresCount = totalStoresCount - activeStoresCount;
const activePct = totalStoresCount > 0 ? Math.round((activeStoresCount / totalStoresCount) * 100) : 0;
const circumference = 251.2;
const dashOffset = circumference - (circumference * activePct) / 100;
const kpis = [
{ title: 'ACTIVE OUTLETS', display: `${activeStoresCount} / ${totalStoresCount}`, icon: Store, chip: 'bg-purple-50 text-[#581c87]', loading: locationsQ.isLoading },
{ title: 'REGION FULFILLMENT', display: '98.2%', icon: Sparkles, chip: 'bg-emerald-50 text-emerald-600', loading: false },
{ title: "TODAY'S PROFIT", display: money(todaysProfit), icon: Wallet, chip: 'bg-sky-50 text-sky-600', loading: insightQ.isLoading },
{ title: 'MONTHLY PROFIT', display: money(monthlyProfit), icon: TrendingUp, chip: 'bg-emerald-50 text-emerald-600', loading: insightQ.isLoading },
];
const statusRows = [
{ label: 'Active Outlets', value: activeStoresCount, dot: 'bg-emerald-500' },
{ label: 'Inactive / Maintenance', value: inactiveStoresCount, dot: 'bg-zinc-400' },
];
const loading = summaryQ.isLoading;
const errored = summaryQ.isError;
return (
<div className="space-y-lg animate-in fade-in duration-500 relative">
{/* Scope banner */}
<div className="bg-[#faf5ff] border border-purple-100 rounded-xl p-md flex items-center justify-between shadow-[0_1px_3px_rgba(0,0,0,0.05)]">
<div className="flex items-center gap-sm">
<Sparkles size={16} className="text-[#581c87]" />
<span className="font-sans text-xs text-zinc-700 font-medium">
Live operations data for <strong>{tenantName}</strong> · {fromdate} {todate}
</span>
</div>
</div>
{/* Header */}
<div className="flex justify-between items-end">
<div>
<h1 className="font-sans font-bold text-3xl tracking-tight text-[#0f172a]">Executive Command Center</h1>
<div className="flex items-center gap-2 mt-1">
<p className="text-zinc-500 font-sans text-sm">Month-to-date order operations, pulled live from the API.</p>
{loading ? (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-zinc-400 uppercase tracking-wide">
<span className="w-1.5 h-1.5 rounded-full bg-zinc-300 animate-pulse" /> Loading
</span>
) : errored ? (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-rose-600 uppercase tracking-wide" title="Restart the dev server so the /hasura proxy is active.">
<span className="w-1.5 h-1.5 rounded-full bg-rose-500" /> Live data unavailable
</span>
) : (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-emerald-600 uppercase tracking-wide">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" /> Live · {tenantName}
</span>
)}
</div>
</div>
</div>
{/* Error hint */}
{errored && (
<div className="bg-rose-50 border border-rose-200 rounded-xl p-md flex items-start gap-sm text-xs text-rose-800">
<AlertTriangle size={16} className="shrink-0 mt-0.5 text-rose-500" />
<div>
<p className="font-semibold">Couldn't reach the live API.</p>
<p className="mt-0.5 text-rose-700">
The <code>/hasura</code> dev proxy loads at server start — stop and re-run <code>npm run dev</code> so the
secret and proxy are active.
</p>
</div>
</div>
)}
{/* KPI cards — all live from getordersummary */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-gutter">
{kpis.map((kpi) => {
const Icon = kpi.icon;
return (
<div
key={kpi.title}
className="group relative flex flex-col bg-white border border-[#eceef2] rounded-xl p-3 shadow-[0_1px_2px_rgba(16,24,40,0.04)] transition-all duration-200 hover:-translate-y-0.5 hover:border-purple-300 hover:shadow-[0_8px_22px_rgba(16,24,40,0.08)]"
>
<div className={`h-7 w-7 rounded-lg flex items-center justify-center ${kpi.chip}`}>
<Icon size={14} />
</div>
<p className="text-[10px] font-semibold text-zinc-400 tracking-wider uppercase font-sans mt-3">
{kpi.title}
</p>
<p className="font-sans font-bold text-2xl leading-tight text-[#0f172a] tracking-tight mt-0.5">
{kpi.loading ? <span className="text-zinc-300">…</span> : kpi.display}
</p>
</div>
);
})}
</div>
{/* Order status + store locations */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-gutter">
{/* Store Node Status donut (live) */}
<div className="bg-white border border-[#e2e8f0] rounded-xl p-md flex flex-col shadow-[0_1px_3px_rgba(0,0,0,0.05)]">
<div>
<h3 className="font-sans font-bold text-base text-[#0f172a]">Store Outlet Status</h3>
<p className="text-zinc-500 text-xs font-sans mt-0.5">Active share of all registered store nodes.</p>
</div>
<div className="my-md flex justify-center items-center">
<div className="relative w-40 h-40 flex items-center justify-center">
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="transparent" stroke="#eceef0" strokeWidth="8" />
<circle
cx="50"
cy="50"
r="40"
fill="transparent"
stroke="#10b981"
strokeWidth="8"
strokeDasharray={circumference}
strokeDashoffset={dashOffset}
strokeLinecap="round"
className="transition-all duration-700"
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="font-sans font-bold text-3xl text-[#0f172a] tracking-tight">{activePct}%</span>
<span className="text-[10px] text-emerald-600 uppercase tracking-widest font-semibold mt-1">Active</span>
</div>
</div>
</div>
<div className="divide-y divide-[#f1f5f9] text-xs">
{statusRows.map((r) => (
<div key={r.label} className="flex justify-between items-center py-2">
<span className="flex items-center gap-1.5 text-zinc-500">
<span className={`w-2.5 h-2.5 rounded-full ${r.dot}`} />
{r.label}
</span>
<span className="font-mono font-bold text-zinc-700">{r.value.toLocaleString('en-IN')}</span>
</div>
))}
<div className="flex justify-between items-center py-2">
<span className="text-zinc-500 font-semibold">Total Nodes</span>
<span className="font-mono font-bold text-[#581c87]">{totalStoresCount.toLocaleString('en-IN')}</span>
</div>
</div>
</div>
{/* Store locations (live) */}
<div className="lg:col-span-2 bg-white border border-[#e2e8f0] rounded-xl p-md shadow-[0_1px_3px_rgba(0,0,0,0.05)]">
<div className="flex justify-between items-center mb-md pb-xs border-b border-[#f1f5f9]">
<h3 className="font-sans font-bold text-base text-[#0f172a] flex items-center gap-2">
<Store size={16} className="text-[#581c87]" /> Store Locations
</h3>
<span className="text-[10px] text-[#581c87] uppercase font-bold bg-purple-50 px-2 py-0.5 rounded tracking-wide border border-purple-100">
{locationsQ.isLoading ? 'Loading' : `${locations.length} Outlet${locations.length === 1 ? '' : 's'}`}
</span>
</div>
{locationsQ.isLoading ? (
<div className="text-center py-xl text-zinc-400 text-xs">Loading store locations…</div>
) : locations.length === 0 ? (
<div className="text-center py-xl text-zinc-400 text-xs">No store locations found for this tenant.</div>
) : (
<div className="space-y-sm max-h-80 overflow-y-auto">
{locations.map((loc, i) => {
const sum = summaries.find((s) => s.locationid === Number(loc.locationid));
const deliveries = sum?.delivered ?? 0;
const orders = Math.max(sum?.delivered ?? 0, sum?.total ?? 0);
return (
<div
key={str(loc.locationid) || i}
className="p-sm border border-[#e2e8f0] rounded-lg bg-[#f8fafc]/40 flex justify-between items-start gap-md hover:border-purple-200 transition-colors animate-in fade-in"
>
<div className="min-w-0">
<p className="font-sans font-semibold text-sm text-[#0f172a] truncate">{str(loc.locationname)}</p>
<p className="text-[11px] text-zinc-500 mt-0.5 flex items-center gap-1">
<MapPin size={11} className="shrink-0 text-zinc-400" />
<span className="truncate">{str(loc.address) || `${str(loc.suburb)}, ${str(loc.city)}`}</span>
</p>
{str(loc.contactno) && (
<p className="text-[11px] text-zinc-500 mt-0.5 flex items-center gap-1">
<Phone size={11} className="shrink-0 text-zinc-400" />
{str(loc.contactno)}
</p>
)}
{/* Node-specific Orders and Dispatches */}
<div className="flex items-center gap-3 mt-2.5">
<span className="text-[10px] bg-purple-50 text-[#581c87] font-semibold px-2 py-0.5 rounded border border-purple-100/50">
{orders} Orders
</span>
<span className="text-[10px] bg-emerald-50 text-emerald-700 font-semibold px-2 py-0.5 rounded border border-emerald-100/50">
{deliveries} Dispatched
</span>
{orders > 0 && (
<span className="text-[10px] text-zinc-400 font-medium">
{Math.round((deliveries / orders) * 100)}% Fulfilled
</span>
)}
</div>
</div>
<span
className={`shrink-0 px-1.5 py-0.5 rounded text-[9px] font-bold uppercase ${
str(loc.status).toLowerCase() === 'active'
? 'text-emerald-600 bg-emerald-50 border border-emerald-100'
: 'text-zinc-500 bg-zinc-100'
}`}
>
{str(loc.status) || ''}
</span>
</div>
);
})}
</div>
)}
</div>
</div>
</div>
);
}