import { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Bell, CalendarDays, CheckCircle2, ChevronDown, IndianRupee, Package, Sparkles } from 'lucide-react'; import { useQuery, useZino } from '../../api/provider'; import { RV } from '../../api/config'; import type { RecRow } from '../../api/types'; import { Bar, Button, Chip, ErrorNote, Input, Label, Select, Sheet, Spinner } from '../../components/core'; import { useSimulate } from '../../components/widgets'; import { date, inr, mt, num, parseJson } from '../../format'; import { placeOneTap, plusDays, useDealer, useDealerRow, useRecommend } from './useDealer'; import type { Opt } from './useDealer'; const KIND_TITLE: Record = { TOP_UP: 'Top up', COLLECTION: 'Clear dues', SWITCH_TO_PP: 'Go premium', ROUTE_SHIP_TO: 'Change ship-to', BOOK_IN_WINDOW: 'Book later', NOTHING_TO_DO: 'All good' }; /** Today: three numbers, then the cards. Each card is one action and one button. */ export function Today() { const { client, portal } = useZino(); const navigate = useNavigate(); const cust = portal?.customer_no; const d = useDealer(cust); const row = useDealerRow(cust); const recs = useRecommend(cust); const sim = useSimulate(cust, null); const upcoming = useQuery(() => client.rows(RV.UPCOMING, { params: { customer_no: cust } }), [cust], !!cust); const [act, setAct] = useState(null); const [reminded, setReminded] = useState>(() => { try { return JSON.parse(localStorage.getItem('meru-reminders') ?? '{}'); } catch { return {}; } }); if (!portal) return ; if (!cust) return This login is not mapped to a dealer.; if (d.loading && !d.data) return ; if (d.error) return {d.error}; const s = d.data?.snapshot; if (!s) return No snapshot for {cust}.; const asOf = d.data?.as_of ?? ''; const monthEnd = asOf ? new Date(new Date(asOf).getFullYear(), new Date(asOf).getMonth() + 1, 0) : null; const daysLeft = monthEnd && asOf ? Math.round((monthEnd.getTime() - new Date(asOf).getTime()) / 86400000) : null; const earning = (sim.data ?? []).reduce((a, r) => a + num(r.payout_now), 0); const cards = (recs.data ?? []).filter((r) => r.kind !== 'NOTHING_TO_DO'); const nothing = (recs.data ?? []).find((r) => r.kind === 'NOTHING_TO_DO'); function remind(r: RecRow) { const next = { ...reminded, [r.kind + (r.book_on ?? '')]: true }; setReminded(next); try { localStorage.setItem('meru-reminders', JSON.stringify(next)); } catch { /* fine */ } } return (

{greeting()}, {firstName(s.name)}

{daysLeft != null ? `${daysLeft} days left in ${monthName(asOf)}` : date(asOf)} · {s.cluster} cluster

This month
{mt(s.mtd_mt, 0)}
of {mt(s.month_target_mt, 0)}
= 100000 })} sub="this period" tone="accent" /> 0 ? `${inr(s.overdue, { compact: true })} overdue` : 'none overdue'} tone={s.overdue > 0 ? 'rose' : undefined} />

Do this today

{recs.loading && !recs.data && } {recs.error && {recs.error}}
{cards.map((r) => ( { if (r.action === 'ORDER' || r.action === 'ORDER_PP') setAct(r); else if (r.action === 'PAY') navigate('/d/earnings#dues'); else remind(r); }} /> ))} {recs.data && cards.length === 0 && (
{nothing?.headline ?? 'You are on the best slab this month'}

{nothing?.detail ?? 'No extra order or payment earns more right now.'}

)}
{(upcoming.data ?? []).length > 0 && (

Coming up

    {(upcoming.data ?? []).slice(0, 3).map((u) => (
  • {String(u.scheme_name)}
    {date(u.validity_from)} – {date(u.validity_to)} · in {String(u.starts_in_days)} days
  • ))}
)} {act && ( setAct(null)} cust={cust} dealerName={s.name} asOf={asOf} row={row.data} onPlaced={(id) => { setAct(null); navigate(`/d/orders/${id}?placed=1`); }} /> )}
); } function ActionCard({ rec, onAct, reminded }: { rec: RecRow; onAct: () => void; reminded: boolean }) { const [why, setWhy] = useState(false); const inc = num(rec.incremental); const label = rec.action === 'ORDER' ? `Order ${mt(rec.delta_mt, 0)}` : rec.action === 'ORDER_PP' ? `Order ${mt(rec.delta_pp_mt, 0)} premium` : rec.action === 'PAY' ? 'See dues' : reminded ? 'Reminder set' : 'Remind me'; const Icon = rec.action === 'PAY' ? IndianRupee : rec.action === 'REMIND' ? Bell : Package; const movers = String(rec.movers ?? '').split(', ').filter(Boolean); return (
{KIND_TITLE[rec.kind] ?? rec.kind}{rec.deadline ? ` · by ${shortDate(rec.deadline)}` : ''} {rec.scheme_name ? String(rec.scheme_name).replace(/\s*–\s*AO-?.*$/, '') : ''}

{rec.headline}

{inr(inc, { sign: true, compact: inc >= 100000 })}
{why && (
    {movers.length === 0 &&
  • {rec.detail}
  • } {movers.slice(0, 5).map((m) => { const i = m.lastIndexOf(' '); return
  • {m.slice(0, i)}{m.slice(i + 1).replace(/^\+/, '+₹').replace(/^−/, '−₹')}
  • ; })}
  • Now {inr(rec.payout_now)} → {inr(rec.payout_if)} if you act. Numbers from the scheme engine, as of today.
)}
); } /** The confirm step: what, where, when — one big button. Lands as a CONFIRMED order in the estate. */ export function OneTapSheet({ rec, onClose, cust, dealerName, asOf, row, onPlaced }: { rec: RecRow; onClose: () => void; cust: string; dealerName: string; asOf: string; row: Record | null; onPlaced: (id: number | string) => void; }) { const { client } = useZino(); const materials = useMemo(() => parseJson(row?.materials_json, []), [row]); const shipTos = useMemo(() => parseJson(row?.ship_tos_json, []), [row]); const premium = rec.action === 'ORDER_PP'; const qty = num(premium ? rec.delta_pp_mt : rec.delta_mt); const defaultMat = premium ? (materials.find((m) => m.premium)?.code ?? '') : String(row?.default_material ?? materials[0]?.code ?? ''); const [material, setMaterial] = useState(defaultMat); const [shipTo, setShipTo] = useState(rec.ship_to || String(row?.default_ship_to ?? shipTos[0]?.code ?? '')); const [when, setWhen] = useState(plusDays(asOf, 3)); const [busy, setBusy] = useState(false); const [err, setErr] = useState(null); const mat = material || defaultMat; const plant = String(row?.default_plant ?? ''); const matName = materials.find((m) => m.code === mat)?.name ?? mat; async function place() { if (!mat || !shipTo || qty <= 0) { setErr('Pick a material and a ship-to'); return; } setBusy(true); setErr(null); try { const res = await placeOneTap(client, { customer_no: cust, dealer_name: dealerName, ship_to_code: shipTo, plant_code: plant, material_code: mat, qty_mt: qty, requested_date: when, as_of: asOf, advice_kind: rec.kind, advice_headline: rec.headline, incremental_payout: num(rec.incremental), suggested_scheme: rec.scheme_name, advice_deadline: rec.deadline ? String(rec.deadline).slice(0, 10) : null, advice_accepted: true, }); onPlaced(res.instance_id ?? ''); } catch (e) { setErr((e as { message?: string })?.message ?? 'Could not place the order'); } finally { setBusy(false); } } return (
{err && {err}}
{inr(num(rec.incremental), { sign: true })} more this period · {rec.headline}
{materials.length > 1 ? ({ value: s.code, label: s.name }))} /> :
{shipTos[0]?.name ?? shipTo}
}

Confirmed straight away and sent to Meru as sales order.

); } function shortDate(v: unknown): string { const d = new Date(String(v)); return Number.isNaN(d.getTime()) ? String(v ?? '') : d.toLocaleDateString('en-IN', { day: 'numeric', month: 'short' }); } function greeting(): string { const h = new Date().getHours(); return h < 12 ? 'Good morning' : h < 17 ? 'Good afternoon' : 'Good evening'; } function firstName(n: string): string { return (n ?? '').replace(/\b(traders|agencies|enterprises|cement|stores|and sons|& sons)\b.*$/i, '').trim().split(' ')[0] || n; } function monthName(iso: string): string { return iso ? new Date(iso).toLocaleDateString('en-IN', { month: 'long' }) : ''; }