diff --git a/public/sw.js b/public/sw.js
index bc8c284..eb18226 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -6,7 +6,7 @@
* queued in localStorage by the app itself (see lib/ledgerStore.ts); this worker
* only guarantees the shell and its assets are reachable.
*/
-const CACHE = 'udyamlinc-v1'
+const CACHE = 'udyamlinc-v2'
const SHELL = ['/', '/app', '/index.html', '/manifest.webmanifest', '/udyamlinc-tile.svg']
self.addEventListener('install', (event) => {
diff --git a/src/features/enterprise/AppAlertsPage.tsx b/src/features/enterprise/AppAlertsPage.tsx
index 7dd2eb6..41a88e3 100644
--- a/src/features/enterprise/AppAlertsPage.tsx
+++ b/src/features/enterprise/AppAlertsPage.tsx
@@ -1,4 +1,5 @@
-import { enterpriseSession } from '../../lib/api'
+import { useQuery } from '@tanstack/react-query'
+import { enterpriseSession, fetchAppAlerts, fetchAppReasons } from '../../lib/api'
import { Empty } from '../../components/ui/bits'
import { findEnterprise, reasonCodesFor, tripWiresFor } from '../../lib/demo/enterprises'
import { alertForEnterprise } from '../../lib/demo/ops'
@@ -15,11 +16,28 @@ export function AppAlertsPage() {
const { lang, t } = useLang('hi')
const hi = lang === 'hi'
+ // Live alerts for this borrower. The token decides whose they are.
+ const alertsQ = useQuery({ queryKey: ["app-alerts"], queryFn: fetchAppAlerts, enabled: Boolean(session) })
+ // The scored reasons, not a sentence generated from the demo sector table.
+ const reasonsQ = useQuery({ queryKey: ["app-reasons"], queryFn: fetchAppReasons, enabled: Boolean(session) })
+
const e = session ? findEnterprise(session.enterpriseId) : undefined
if (!session || !e) return
- const alert = alertForEnterprise(e.enterpriseId)
- const reasons = reasonCodesFor(e)
+ const live = alertsQ.data?.[0]
+ const alert = live
+ ? { ...live, titleHi: live.titleHi ?? live.title, shortfall: live.shortfall ?? 0,
+ breachMonth: live.breachMonth ?? "",
+ reason: { driver: "", driverHi: "", weight: 0,
+ sentenceEn: live.reasonEn ?? "", sentenceHi: live.reasonHi ?? "" } }
+ : alertsQ.isSuccess ? null : alertForEnterprise(e.enterpriseId)
+ // Fall back to the demo sentences only while the request is in flight — once it
+ // has answered, an empty list means this borrower has no scored reasons, and
+ // inventing three would be a lie told on the borrower's own screen.
+ const live_reasons = reasonsQ.data
+ const reasons = live_reasons
+ ? live_reasons.slice(0, 3)
+ : reasonsQ.isSuccess ? [] : reasonCodesFor(e)
const fired = tripWiresFor(e).filter((w) => w.fired)
if (!alert) {
@@ -52,6 +70,11 @@ export function AppAlertsPage() {
{hi ? 'ऐसा क्यों लग रहा है' : 'Why this is expected'}
+ {reasons.length === 0 && (
+ -
+ {hi ? 'इस महीने कोई मापी गई वजह दर्ज नहीं है।' : 'No measured reason is on record for this run.'}
+
+ )}
{reasons.map((r) => (
-
diff --git a/src/features/enterprise/AppHomePage.tsx b/src/features/enterprise/AppHomePage.tsx
index 137a587..f5a8ad6 100644
--- a/src/features/enterprise/AppHomePage.tsx
+++ b/src/features/enterprise/AppHomePage.tsx
@@ -1,5 +1,6 @@
+import { useQuery } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
-import { enterpriseSession } from '../../lib/api'
+import { enterpriseSession, fetchAppHome, fetchAppLedger, toLedgerEntry } from '../../lib/api'
import type { ForecastPoint } from '../../lib/api'
import { Empty } from '../../components/ui/bits'
import { breachPoint, findEnterprise, forecastFor } from '../../lib/demo/enterprises'
@@ -23,13 +24,31 @@ export function AppHomePage() {
const [sheet, setSheet] = useState(null)
const e = session ? findEnterprise(session.enterpriseId) : undefined
- const { entries } = useLedger(session?.enterpriseId ?? '')
+ const ledgerQ = useQuery({ queryKey: ["app-ledger"], queryFn: fetchAppLedger, enabled: Boolean(session) })
+ const server = (ledgerQ.data ?? []).map(toLedgerEntry)
+ const { entries } = useLedger(session?.enterpriseId ?? "", server)
+
+ // Live record for this borrower. The token carries the enterprise id, so the
+ // server decides whose data this is — the app never asks for an id.
+ const homeQ = useQuery({ queryKey: ["app-home"], queryFn: fetchAppHome, enabled: Boolean(session) })
+ const home = homeQ.data
if (!session || !e) return
- const points = forecastFor(e)
- const alert = alertForEnterprise(e.enterpriseId)
- const breach = breachPoint(points)
+ // Server forecast where we have one; the in-file generator only if the API is
+ // unreachable, so a borrower with no signal still sees their own screen.
+ const points = home?.forecast?.length
+ ? home.forecast.map((p) => ({
+ month: p.month,
+ label: monthRef(new Date(`${p.month}-01T00:00:00`)).label,
+ labelHi: monthRef(new Date(`${p.month}-01T00:00:00`)).labelHi,
+ p10: p.p10, p50: p.p50, p90: p.p90, obligation: p.obligation,
+ }))
+ : forecastFor(e)
+ const alert = home?.alert
+ ? { ...home.alert, titleHi: home.alert.titleHi ?? home.alert.title }
+ : alertForEnterprise(e.enterpriseId)
+ const breach = breachPoint(points as never)
const breachRef = breach ? monthRef(new Date(`${breach.month}-01T00:00:00`)) : null
const totals = dayTotals(entries)
const hi = lang === 'hi'
@@ -51,7 +70,7 @@ export function AppHomePage() {
{hi ? alert.titleHi : alert.title}
- {t('home.estimate')}: {inr(alert.shortfall)} {t('home.shortfall')}
+ {t('home.estimate')}: {inr(alert.shortfall ?? 0)} {t('home.shortfall')}
›
diff --git a/src/features/enterprise/AppLedgerPage.tsx b/src/features/enterprise/AppLedgerPage.tsx
index be83586..1fce6df 100644
--- a/src/features/enterprise/AppLedgerPage.tsx
+++ b/src/features/enterprise/AppLedgerPage.tsx
@@ -1,5 +1,6 @@
import { useMemo, useState } from 'react'
-import { enterpriseSession } from '../../lib/api'
+import { useQuery } from '@tanstack/react-query'
+import { enterpriseSession, fetchAppLedger, toLedgerEntry } from '../../lib/api'
import type { LedgerKind } from '../../lib/api'
import { Empty } from '../../components/ui/bits'
import { CATEGORIES, useLedger } from '../../lib/ledgerStore'
@@ -15,7 +16,9 @@ export function AppLedgerPage() {
const { lang, t } = useLang('hi')
const [range, setRange] = useState('WEEK')
const [sheet, setSheet] = useState(null)
- const { entries } = useLedger(session?.enterpriseId ?? '')
+ const ledgerQ = useQuery({ queryKey: ["app-ledger"], queryFn: fetchAppLedger, enabled: Boolean(session) })
+ const server = (ledgerQ.data ?? []).map(toLedgerEntry)
+ const { entries } = useLedger(session?.enterpriseId ?? "", server)
const hi = lang === 'hi'
const cutoff = useMemo(() => {
diff --git a/src/features/enterprise/AppProfilePage.tsx b/src/features/enterprise/AppProfilePage.tsx
index b93d33b..90816a4 100644
--- a/src/features/enterprise/AppProfilePage.tsx
+++ b/src/features/enterprise/AppProfilePage.tsx
@@ -1,6 +1,7 @@
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
-import { clearSessions, enterpriseSession } from '../../lib/api'
+import { clearSessions, enterpriseSession, fetchAppHome, setAppConsent } from '../../lib/api'
import type { Lang } from '../../lib/api'
import { Empty } from '../../components/ui/bits'
import { SECTORS, findEnterprise } from '../../lib/demo/enterprises'
@@ -16,20 +17,41 @@ import { useOffline } from '../../lib/useOffline'
export function AppProfilePage() {
const navigate = useNavigate()
const session = enterpriseSession()
+ const queryClient = useQueryClient()
const { lang, setLang, t } = useLang('hi')
const offline = useOffline()
- const [consent, setConsent] = useState(true)
const [justSynced, setJustSynced] = useState(null)
const hi = lang === 'hi'
- const e = session ? findEnterprise(session.enterpriseId) : undefined
+ // Live record, so the profile shows what the lender actually holds.
+ const homeQ = useQuery({ queryKey: ["app-home"], queryFn: fetchAppHome, enabled: Boolean(session) })
+ const api = homeQ.data?.enterprise
+
+ // Consent is the server's record, not a switch on this screen. Withdrawing it
+ // has to stop scoring everywhere, so the toggle writes and then re-reads.
+ const consent = api ? api.consentActive !== false : true
+ const consentM = useMutation({
+ mutationFn: (next: boolean) => setAppConsent(next),
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['app-home'] }),
+ })
+ const demo = session ? findEnterprise(session.enterpriseId) : undefined
+ const e = api
+ ? { ...(demo ?? ({} as never)),
+ enterpriseId: String(api.enterpriseId ?? ""),
+ name: String(api.name ?? ""), nameHi: String(api.nameHi ?? ""),
+ block: String(api.block ?? ""), ownerCount: Number(api.ownerCount ?? 0),
+ vintageMonths: Number(api.vintageMonths ?? 0),
+ loanOutstanding: Number(api.loanOutstanding ?? 0),
+ monthlyInstalment: Number(api.monthlyInstalment ?? 0),
+ score: Number(api.score ?? 0), band: (api.band ?? "STANDARD") as never }
+ : demo
if (!session || !e) return
const pending = pendingCount(e.enterpriseId)
const synced = lastSyncedAt()
- function doSync() {
- const n = syncPending(e!.enterpriseId)
+ async function doSync() {
+ const n = await syncPending(e!.enterpriseId)
setJustSynced(n)
setTimeout(() => setJustSynced(null), 2200)
}
@@ -76,10 +98,15 @@ export function AppProfilePage() {
: 'Only transaction aggregates are read — never a name, number or account.'}
@@ -96,7 +123,7 @@ export function AppProfilePage() {
{t('profile.lastSynced')}: {synced ? new Date(synced).toLocaleString() : t('profile.never')}