import { useMemo, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { Avatar, BackLink, Badge, Button, Card, EmptyState, Icon, InfoList, InfoRow, PageHeader, } from '../../components/ui' import useDetailView from '../../hooks/useDetailView' import { DETAIL_VIEWS, detailValue } from '../../api/detailView' import { toISODate } from '../../api/recordView' import { instanceState, isEndState } from '../../api/activityForm' import FieldValue, { formatMeta } from '../../components/activity/FieldValue' import { ALL_EMPLOYEE_FIELDS, EMPLOYEE_UPDATE_FIELDS, withLatestInstanceValues, } from '../../api/employees' import { EMPLOYEE_STATUS_TONE } from '../../constants' import UpdateEmployeeModal from './UpdateEmployeeModal' import InactiveEmployeeModal from './InactiveEmployeeModal' const VIEW = DETAIL_VIEWS.employees /** * INIT ids first, Update ids last, so when a record holds both (field_2 and * field_10 for email) the value from the latest Update wins. */ const FIELD_DEFS = ALL_EMPLOYEE_FIELDS /** * Employee details, read live from the workflow's detail view. * Route: /employees/:instanceId — the id of the employee's workflow instance. */ export default function EmployeeDetails() { const { instanceId } = useParams() const navigate = useNavigate() // The detail template can return old values after an Update, so merge in // the workflow instance, which always holds what was saved. const { view, loading, error, reload } = useDetailView(VIEW.templateUid, instanceId, { fieldDefs: FIELD_DEFS, enrich: withLatestInstanceValues, }) const [updating, setUpdating] = useState(false) const [inactivating, setInactivating] = useState(false) const [notice, setNotice] = useState(null) // Row for the Update dialog: values keyed by field label (what it pre-fills // from) plus the instance id, so it skips the lookup. const updateRow = useMemo(() => { if (!view) return null const row = {} for (const section of view.sections) { for (const f of section.fields) if (f.value != null) row[f.label] = f.value } for (const def of FIELD_DEFS) { const v = view.record[def.id] if (v !== undefined && v !== null && v !== '') row[def.label] = v } return { ...row, instance_id: instanceId } }, [view, instanceId]) const back = () => navigate('/employees') if (loading && !view) { return ( <> Back to Employees ) } if (error || !view) { return ( <> Back to Employees } /> ) } const firstName = detailValue(view, 'first_name', 'first name') const lastName = detailValue(view, 'last_name', 'last name') const name = [firstName, lastName].filter(Boolean).join(' ').trim() || 'Employee' const designation = detailValue(view, 'designation') const department = detailValue(view, 'department') const code = detailValue(view, 'employee_code', 'employee code', 'ID Generator') const status = detailValue(view, 'status') const email = detailValue(view, 'email') const phone = detailValue(view, 'phone') // The instance carries current_state_name. Inactive moves it to End State, // which rejects every activity, so don't offer them there. const finished = isEndState(view.record) const subtitle = [designation, department, code].filter(Boolean).join(' · ') const empty = view.sections.every((s) => s.fields.length === 0) /** `values` is keyed by the update field ids — see EMPLOYEE_UPDATE_FIELDS. */ async function handleUpdated(values) { setNotice(null) const next = await reload() // Read the employee back and say plainly if any saved value didn't come back. const stale = next ? staleFields(next, values) : [] setNotice( stale.length ? { tone: 'warning', text: `Saved, but the workflow still returns the old value for ${stale.join(', ')}. ` + 'The Zino views are not reading these Update fields yet.', } : { tone: 'success', text: 'Employee was updated.' }, ) } async function handleInactivated() { setNotice({ tone: 'success', text: `${name} was marked inactive.` }) await reload() } return ( <> Back to Employees } /> {finished && (
This employee has been marked inactive, so the workflow is in " {instanceState(view.record)}" and has finished. It can't be updated or marked inactive again.
)} {notice && (
{notice.text}
)}
{/* ---------- Left: profile + record info ---------- */}
{name}
{designation &&
{designation}
} {status && {String(status)}}
{email ? {String(email)} : null} {phone != null ? String(phone) : null} {code != null ? String(code) : null}
{view.meta.length > 0 && ( {view.meta.map((m) => ( {formatMeta(m.value)} ))} )}
{/* ---------- Right: every field the detail view returns ---------- */}
{empty ? ( ) : ( view.sections.map((section, i) => (
{section.fields.map((f) => (
{f.label}
))}
)) )}
Raw response
              {JSON.stringify({ detail_view: view.raw, workflow_instance: view.instance ?? null }, null, 2)}
            
setUpdating(false)} onUpdated={handleUpdated} /> setInactivating(false)} onDone={handleInactivated} /> ) } /** Labels of the fields that were sent but don't read back with that value. */ function staleFields(view, values) { const norm = (v, type) => { if (type === 'date') return toISODate(v) if (type === 'number') return String(v ?? '').replace(/[^\d.-]/g, '') return String(v ?? '').trim().toLowerCase() } return EMPLOYEE_UPDATE_FIELDS.filter((f) => { const sent = values?.[f.id] if (sent === undefined || sent === null || sent === '') return false return norm(detailValue(view, f.label), f.type) !== norm(sent, f.type) }).map((f) => f.label) } /** Status badge tone, matched case-insensitively ("active" -> success). */ function statusTone(status) { const key = Object.keys(EMPLOYEE_STATUS_TONE).find( (k) => k.toLowerCase() === String(status).trim().toLowerCase(), ) return key ? EMPLOYEE_STATUS_TONE[key] : 'neutral' }