diff --git a/src/api/config.js b/src/api/config.js index 6ce7083..58f1e2e 100644 --- a/src/api/config.js +++ b/src/api/config.js @@ -96,67 +96,79 @@ export const DV_LEAD = 'zk-dv-lead' * the platform allows it, the console never offers it, and nothing errors. * Collect Documents shipped in that state for one round. */ +/** + * What each activity IS at a stage, not merely who may press it. + * + * `do` the expected next step here. Usually one; Referred has two, + * because clear and decline are a decision pair rather than a + * step and an alternative to it. + * `again` a bounded loop — a retry, a reminder, a re-quote. Legitimate, + * never the thing to do next. + * `force` an AI employee's own job, offered to a person ONLY so a + * stalled lead can be pushed by hand. Presenting these as + * actions is what made the app read as a control panel: six + * equal buttons where five are recovery levers. + * `exit` Mark Lost. Always reachable, never a step. Four stages used + * to offer a partner agent this and nothing else, which told + * them their only option was to give up on a lead the system + * was actively working. + */ const STATE_ACTIVITIES = { - 'zk-state-new': [{ uid: 'zk-act-qualify', label: 'Qualify Lead', by: 'Intake AI' }], - // Retry Call is scheduled automatically four hours after an unanswered - // call, clamped into 09:00-21:00 IST and bounded at three attempts. It - // records the attempt; whether it also DIALS is gated on the lead's - // `auto_retry` field, which ships unset. Offered here so a person can - // also place the retry by hand. - 'zk-state-qualified': [{ uid: 'zk-act-contact', label: 'Log Contact', by: 'Engage AI' }, - { uid: 'zk-act-retry-call', label: 'Retry Call', by: 'Scheduled — or you' }], - // Contacted has exactly one way forward and it belongs to Engage. An AI - // cannot receive a file, so it writes the ask; the agent uploads one state - // later. Giving the AI the activity that RECEIVES documents is how you get an - // agent recording documents it never saw. - 'zk-state-contacted': [{ uid: 'zk-act-request-docs', label: 'Request Documents', by: 'Engage AI' }], - // Four steps that used to be two stages: the upload, the capture, the advice - // and the quote. The lead stays here for all of them and leaves only once a - // premium exists. - 'zk-state-docs': [{ uid: 'zk-act-collect-docs', label: 'Collect Documents', by: 'Anyone — upload here' }, - { uid: 'zk-act-capture-motor', label: 'Capture Motor Risk', by: 'Engage AI' }, - { uid: 'zk-act-capture-sme', label: 'Capture SME Risk', by: 'Engage AI' }, - { uid: 'zk-act-advise', label: 'AI Cover Recommendation', by: 'Advisor AI' }, - { uid: 'zk-act-quote', label: 'Generate Quote', by: 'Rating engine' }, - // Fires on its own at 24h, 72h and 120h from the ask. On the - // third unanswered one the lead leaves this queue — Parked if - // the renewal is still weeks off, Lost if the cover is going. - { uid: 'zk-act-doc-reminder', label: 'Send Document Reminder', by: 'Scheduled — or you' }], - // Likewise absorbs acceptance, identity and the underwriting screen — none of - // which was ever a stage a lead rested in. Generate Quote appears again as a - // RE-quote for a customer who wants different cover; the lead does not move, - // and quote_revisions counts them (0 on the first, 2 is the ceiling). - 'zk-state-quoted': [{ uid: 'zk-act-accept', label: 'Accept Proposal', by: 'Customer' }, - { uid: 'zk-act-kyc', label: 'Verify KYC', by: 'KYC AI' }, - { uid: 'zk-act-uw-screen', label: 'Underwriting Screen', by: 'KYC AI' }, - { uid: 'zk-act-quote', label: 'Generate Quote (revise)', by: 'Engage AI' }, - { uid: 'zk-act-collect-docs', label: 'Collect Documents', by: 'Anyone — self-loop' }], - 'zk-state-referred': [{ uid: 'zk-act-uw-prepare', label: 'Prepare Referral', by: 'Referral AI' }, - { uid: 'zk-act-uw-clear', label: 'Clear Referral', by: 'Underwriter' }, - { uid: 'zk-act-uw-decline', label: 'Decline Referral', by: 'Underwriter' }], - // 'Payment webhook' was aspirational — there is no payment integration on the - // platform and none is planned here. Collection happens on the insurer's own - // rails; ops records the reference. See 35_reframe_to_what_exists.sql. - // - // Request Premium moved here when Underwriting Cleared was removed: entering - // this state IS the decision to collect, so the ask happens inside it. - 'zk-state-payment': [{ uid: 'zk-act-payment', label: 'Request Premium', by: 'Engage AI' }, - { uid: 'zk-act-nudge', label: 'Log Payment Nudge', by: 'Engage AI' }, - { uid: 'zk-act-realise', label: 'Confirm Premium Realisation', by: 'Ops' }], - 'zk-state-issued': [{ uid: 'zk-act-onboard', label: 'Complete Onboarding', by: 'Engage AI' }], - 'zk-state-parked': [{ uid: 'zk-act-resume', label: 'Resume Outreach', by: 'Whoever owns the book' }], + 'zk-state-new': [ + { uid: 'zk-act-qualify', label: 'Qualify Lead', by: 'Intake AI', role: 'force' }, + ], + 'zk-state-qualified': [ + { uid: 'zk-act-contact', label: 'Log Contact', by: 'Engage AI', role: 'force' }, + { uid: 'zk-act-retry-call', label: 'Retry Call', by: 'Scheduled', role: 'again' }, + ], + 'zk-state-contacted': [ + { uid: 'zk-act-request-docs', label: 'Request Documents', by: 'Engage AI', role: 'force' }, + ], + // The one stage whose next step is a person's: three documents have to + // be found and uploaded. Everything else here is the AI's own chain. + 'zk-state-docs': [ + { uid: 'zk-act-collect-docs', label: 'Upload documents', by: 'you', role: 'do' }, + { uid: 'zk-act-doc-reminder', label: 'Send a reminder', by: 'Scheduled', role: 'again' }, + { uid: 'zk-act-capture-motor', label: 'Capture Motor Risk', by: 'Engage AI', role: 'force' }, + { uid: 'zk-act-capture-sme', label: 'Capture SME Risk', by: 'Engage AI', role: 'force' }, + { uid: 'zk-act-advise', label: 'AI Cover Recommendation', by: 'Advisor AI', role: 'force' }, + { uid: 'zk-act-quote', label: 'Generate Quote', by: 'Rating engine', role: 'force' }, + ], + 'zk-state-quoted': [ + { uid: 'zk-act-accept', label: 'Record the acceptance', by: 'you, on consent', role: 'do' }, + { uid: 'zk-act-quote', label: 'Re-quote', by: 'Engage AI', role: 'again' }, + { uid: 'zk-act-collect-docs', label: 'Add a document', by: 'you', role: 'again' }, + { uid: 'zk-act-kyc', label: 'Verify KYC', by: 'KYC AI', role: 'force' }, + { uid: 'zk-act-uw-screen', label: 'Underwriting Screen', by: 'KYC AI', role: 'force' }, + ], + 'zk-state-referred': [ + { uid: 'zk-act-uw-clear', label: 'Clear the referral', by: 'you', role: 'do' }, + { uid: 'zk-act-uw-decline', label: 'Decline the risk', by: 'you', role: 'do' }, + { uid: 'zk-act-uw-prepare', label: 'Prepare Referral', by: 'Referral AI', role: 'force' }, + ], + 'zk-state-payment': [ + { uid: 'zk-act-realise', label: 'Confirm premium received', by: 'you', role: 'do' }, + { uid: 'zk-act-nudge', label: 'Chase the payment', by: 'Engage AI', role: 'again' }, + { uid: 'zk-act-payment', label: 'Request Premium', by: 'Engage AI', role: 'force' }, + ], + 'zk-state-issued': [ + { uid: 'zk-act-onboard', label: 'Complete Onboarding', by: 'Engage AI', role: 'force' }, + ], + 'zk-state-parked': [ + { uid: 'zk-act-resume', label: 'Resume outreach now', by: 'you', role: 'do' }, + ], } /** * Mark Lost is not tied to one state: a lead can be dropped from anywhere before - * the policy issues, and every role that files a lead may do it. It was missing - * from the table entirely, which is the exact failure the note above describes — - * the platform allowed it, the console never offered it, and nothing errored. + * the policy issues, and every role that files a lead may do it. Appended rather + * than written into every entry so there is one place to change it, and so a + * stage added above cannot silently lose it. * - * Appended rather than written into all ten entries so there is one place to - * change it, and so a state added above cannot silently lose it. + * It carries role 'exit' precisely so it is never rendered as a step. It is the + * way out, not the way on. */ -const DROP = { uid: 'zk-act-drop', label: 'Mark Lost', by: 'Whoever holds the lead' } +const DROP = { uid: 'zk-act-drop', label: 'Mark Lost', by: 'whoever holds the lead', role: 'exit' } const DROPPABLE = [ 'zk-state-new', 'zk-state-qualified', 'zk-state-contacted', 'zk-state-docs', diff --git a/src/screens/Lead.jsx b/src/screens/Lead.jsx index 72d6c66..1f1f13b 100644 --- a/src/screens/Lead.jsx +++ b/src/screens/Lead.jsx @@ -341,42 +341,113 @@ export default function Lead() { ) : null} - {actions.length ? ( -
Available to your role at this stage.
-+ {step.length + ? 'This is what this lead is waiting on.' + : isClosed + ? 'This lead is closed.' + : `Nothing is waiting on you — ${stage?.by ?? 'someone else'} has this one.`} +
++ {force.length + ? 'These normally run by themselves. Use one only if this lead has been sitting longer than it should.' + : 'Close this lead if it is going nowhere.'} +
+