import { useMemo, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import { CalendarClock, CloudRain, Database, Siren, TrendingDown } from 'lucide-react'
import { get } from '../../lib/api'
import type { ClimateSignal, Enterprise, MarketSignal, PortfolioSummary } from '../../lib/api'
import { Unavailable } from '../../components/ui/bits'
import { SECTORS } from '../../lib/demo/enterprises'
import { demoClimateSignals, demoMarketSignals } from '../../lib/demo/feeds'
import { monthRef } from '../../lib/demo/seed'
import { exportCsv } from '../../lib/csv'
import { useScope } from './officerScope'
import { ScheduleVisitModal } from './ScheduleVisitModal'
import { DashboardTabs } from './DashboardTabs'
/**
* The officer's morning screen, organised by **when trouble arrives**.
*
*
The previous version ranked by score: five tiles counting bands, then forty
* rows sorted by composite score. That is what a bureau score already shows. The
* thing this platform can do that a bureau cannot is say when — trouble
* four months out, while intervention is still cheap — and nothing on that screen
* expressed time at all.
*
*
So the primary axis here is the horizon at which the downside case first
* fails to cover the instalment. The triage strip is a funnel, the chart draws
* the intervention window, and the list is a day's work rather than a register.
* The full register still lives at /officer/enterprises; this screen
* deliberately does not duplicate it.
*
*
The old layout is kept at /officer/classic — it is verified and
* this one is new, and on a demo day that switch is worth having.
*/
type Bucket = 'NOW' | 'WINDOW' | 'LATER' | 'CLEAR'
const BUCKETS: { key: Bucket; label: string; caption: string; tone: string }[] = [
{ key: 'NOW', label: 'Act now', caption: 'breaching this month', tone: 'gf-now' },
{ key: 'WINDOW', label: 'Month 2–4', caption: 'the window that matters', tone: 'gf-window' },
{ key: 'LATER', label: 'Month 5–6', caption: 'on the horizon', tone: 'gf-watch' },
{ key: 'CLEAR', label: 'Downside covered', caption: 'no breach forecast', tone: 'gf-clear' },
]
/**
* Which bucket a borrower falls in — **on breach horizon alone**.
*
*
Deliberately one dimension. An earlier version mixed band into the last two
* segments, which put every WATCH-band borrower with no forecast breach into
* "Watching" and left "Healthy" reading zero — a funnel that bulged in the middle
* and looked broken.
*
*
Horizon also has to win over score on its own merits. A STANDARD-band
* enterprise whose downside stops covering the instalment in four months is the
* entire point of this product; ranking by current score would bury it under
* borrowers already in arrears, who are already known to everyone.
*/
function bucketOf(e: Enterprise): Bucket {
const h = e.firstBreachHorizon
if (h == null) return 'CLEAR'
if (h <= 1) return 'NOW'
if (h <= 4) return 'WINDOW'
return 'LATER'
}
export function OfficerDashboard() {
const navigate = useNavigate()
const { block, month } = useScope()
const [filter, setFilter] = useState('ALL')
const [scheduling, setScheduling] = useState(null)
const { data, isError, refetch } = useQuery({
queryKey: ['portfolio', block, month],
queryFn: () => get('/officer/portfolio', { block, month }),
})
// No demo fallback: the demo portfolio carries no owner, so substituting it
// would show this bank every other bank's borrowers.
const rows = useMemo(() => data?.rows ?? [], [data])
const counts = useMemo(() => {
const c: Record = { NOW: 0, WINDOW: 0, LATER: 0, CLEAR: 0 }
rows.forEach((e) => { c[bucketOf(e)] += 1 })
return c
}, [rows])
// How many borrowers first breach at each horizon — the intervention window, drawn.
const horizon = useMemo(() => {
const h = [0, 0, 0, 0, 0, 0]
rows.forEach((e) => {
const n = e.firstBreachHorizon
if (n != null && n >= 1 && n <= 6) h[n - 1] += 1
})
return h
}, [rows])
// The day's work: soonest breach first, then worst score. Capped, because an
// officer visits nine borrowers a week and a list of forty is a list of none.
const worklist = useMemo(() => {
const pool = filter === 'ALL'
? rows.filter((e) => bucketOf(e) !== 'CLEAR')
: rows.filter((e) => bucketOf(e) === filter)
return [...pool]
.sort((a, b) =>
(a.firstBreachHorizon ?? 99) - (b.firstBreachHorizon ?? 99) || b.score - a.score)
.slice(0, 9)
}, [rows, filter])
if (isError || !data) return void refetch()} />
const maxH = Math.max(1, ...horizon)
return (
{/* ── triage funnel ─────────────────────────────────────
One strip, four segments, urgency decreasing left to right. A row of
separate tiles counting bands reads as a census; this reads as a queue. */}