11"use client" ;
22
33import { useEffect , useRef , useState } from "react" ;
4+ import { z } from "zod" ;
45
56import { Badge } from "@/components/ui/badge" ;
67import {
@@ -14,7 +15,8 @@ import { Card, CardHeader, CardTitle, Eyebrow } from "@/components/ui/card";
1415import { WorkflowEditor } from "@/components/workflow-editor" ;
1516import { WorkflowGraph } from "@/components/workflow-graph" ;
1617import { API_ROUTES } from "@/lib/api-routes" ;
17- import type { ApprovalWorkflow } from "@/lib/approval-workflow" ;
18+ import { ApprovalWorkflow } from "@/lib/approval-workflow" ;
19+ import { postJson , FetchJsonError } from "@/lib/fetch-json" ;
1820
1921/**
2022 * The onboarding discovery screen — the forward-deployed-engineer step.
@@ -30,34 +32,46 @@ import type { ApprovalWorkflow } from "@/lib/approval-workflow";
3032 * issues before it goes live. (Conversational edits are the next layer.)
3133 */
3234
33- type RoleResolution = {
34- role : string ;
35- title : string ;
36- employeeName : string | null ;
37- rationale : string ;
38- } ;
39- type OrgEmployee = {
40- id : string ;
41- name : string ;
42- title : string ;
43- department : string ;
44- division : string ;
45- managerId : string | null ;
46- } ;
47- type OnboardingResponse = {
48- source : string ;
49- employeeCount : number ;
50- employees : OrgEmployee [ ] ;
51- workflow : ApprovalWorkflow ;
52- proposal : {
53- directorThreshold : number ;
54- roles : RoleResolution [ ] ;
55- summary : string ;
56- } ;
57- issues : { employeeName : string ; detail : string ; note : string } [ ] ;
35+ // The /api/onboarding response, as a Zod schema so we VALIDATE it at the fetch
36+ // boundary (no `res.json() as T`). Types are derived from the schemas.
37+ const OrgEmployee = z . object ( {
38+ id : z . string ( ) ,
39+ name : z . string ( ) ,
40+ title : z . string ( ) ,
41+ department : z . string ( ) ,
42+ division : z . string ( ) ,
43+ managerId : z . string ( ) . nullable ( ) ,
44+ } ) ;
45+ type OrgEmployee = z . infer < typeof OrgEmployee > ;
46+
47+ const RoleResolution = z . object ( {
48+ role : z . string ( ) ,
49+ title : z . string ( ) ,
50+ employeeName : z . string ( ) . nullable ( ) ,
51+ rationale : z . string ( ) ,
52+ } ) ;
53+
54+ const OnboardingResponse = z . object ( {
55+ source : z . string ( ) ,
56+ employeeCount : z . number ( ) ,
57+ employees : z . array ( OrgEmployee ) ,
58+ workflow : ApprovalWorkflow ,
59+ proposal : z . object ( {
60+ directorThreshold : z . number ( ) ,
61+ roles : z . array ( RoleResolution ) ,
62+ summary : z . string ( ) ,
63+ } ) ,
64+ issues : z . array (
65+ z . object ( {
66+ employeeName : z . string ( ) ,
67+ detail : z . string ( ) ,
68+ note : z . string ( ) ,
69+ } ) ,
70+ ) ,
5871 /** Up to three AI-generated next-edit suggestions for the derived workflow. */
59- suggestions : string [ ] ;
60- } ;
72+ suggestions : z . array ( z . string ( ) ) ,
73+ } ) ;
74+ type OnboardingResponse = z . infer < typeof OnboardingResponse > ;
6175
6276type State =
6377 | { status : "idle" }
@@ -71,19 +85,14 @@ export const Onboarding = () => {
7185 const discover = async ( ) => {
7286 setState ( { status : "running" } ) ;
7387 try {
74- const res = await fetch ( API_ROUTES . onboarding , { method : "POST" } ) ;
75- if ( ! res . ok ) {
76- const msg = await res
77- . json ( )
78- . then ( ( j : { error ?: string } ) => j . error )
79- . catch ( ( ) => null ) ;
80- setState ( { status : "error" , message : msg ?? "Discovery failed." } ) ;
81- return ;
82- }
83- const data = ( await res . json ( ) ) as OnboardingResponse ;
88+ const data = await postJson ( API_ROUTES . onboarding , OnboardingResponse ) ;
8489 setState ( { status : "done" , data } ) ;
85- } catch {
86- setState ( { status : "error" , message : "Could not reach the server." } ) ;
90+ } catch ( err ) {
91+ setState ( {
92+ status : "error" ,
93+ message :
94+ err instanceof FetchJsonError ? err . message : "Discovery failed." ,
95+ } ) ;
8796 }
8897 } ;
8998
@@ -213,7 +222,12 @@ const WhatCanIChange = () => {
213222 useEffect ( ( ) => {
214223 if ( ! open ) return ;
215224 const onDoc = ( e : MouseEvent ) => {
216- if ( ref . current && ! ref . current . contains ( e . target as globalThis . Node ) )
225+ const target = e . target ;
226+ if (
227+ target instanceof Node &&
228+ ref . current &&
229+ ! ref . current . contains ( target )
230+ )
217231 setOpen ( false ) ;
218232 } ;
219233 document . addEventListener ( "mousedown" , onDoc ) ;
0 commit comments