import { useCallback, useEffect, useRef, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { AssetStateBadge, BackLink, Button, Card, EmptyState, Icon, InfoList, InfoRow, PageHeader, } from '../../components/ui' import FieldValue, { formatMeta } from '../../components/activity/FieldValue' import { displayLabel } from '../../utils/format' import Timeline from '../../components/common/Timeline' import { instanceState, isEndState } from '../../api/activityForm' import { ASSET_ASSIGN_FIELDS, ASSET_LOST_FIELDS, ASSET_REASSIGN_FIELDS, ASSET_REPAIRED_FIELDS, ASSET_REPAIR_FIELDS, ASSET_RETURN_FIELDS, ASSET_STATE, ASSIGN_EMPLOYEE_FIELD, LOST_EMPLOYEE_FIELD, REASSIGN_EMPLOYEE_FIELD, RETURN_EMPLOYEE_FIELD, assetCode, assetEditValues, assetEvents, assetFieldDef, assetInitValue, currentAssignment, fetchAssetInstance, isAssetState, lookupName, optionLabel, rowFieldValue, withAssetFormColumns, } from '../../api/assets' import { useAuth } from '../../store/AuthContext' import AssetActivityModal from './AssetActivityModal' /** Keys shown as record info rather than as asset fields. */ const META_KEY = /^(instance_id|current_state_name|(created|updated|modified)_\w+)$/i /** * Asset details: the asset's INIT values, its latest assignment and return, * and its workflow state. There is no asset detail view template, so the record comes * from the asset record view (see fetchAssetInstance). * Route: /assets/:assetId — the asset's table id (not its workflow instance id). */ export default function AssetDetails() { const { assetId } = useParams() const navigate = useNavigate() const { logout } = useAuth() const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState('') // The activity dialog that's open: a key of AssetActivityModal's ACTIVITIES, or null. const [activity, setActivity] = useState(null) const [notice, setNotice] = useState(null) // Guards against a slow earlier request overwriting a newer one. const requestId = useRef(0) const load = useCallback(async () => { const id = ++requestId.current setLoading(true) setError('') try { const res = await fetchAssetInstance(assetId) if (id !== requestId.current) return if (!res.row) setError('No asset in the workflow has this id.') setData(res.row ? res : null) } catch (err) { if (id !== requestId.current) return if (err?.status === 401) { logout() navigate('/login', { replace: true }) return } setError(err?.message || 'Could not load this asset.') setData(null) } finally { if (id === requestId.current) setLoading(false) } }, [assetId, logout, navigate]) useEffect(() => { load() }, [load]) const back = () => navigate(-1) if (loading && !data) { return ( <> Back ) } if (error || !data) { return ( <> Back } /> ) } const { config, row } = data // Every Add Asset field, in the form's order and with its labels, then the // view's other columns. const withForm = withAssetFormColumns({ config, data: [row] }) const formRow = withForm.data[0] const assetFields = (withForm.config?.fields ?? []) .filter((f) => f.field_key !== 'id' && !META_KEY.test(f.field_key)) .map((f) => { const label = String(f.output_label || f.field_key).trim() const def = assetFieldDef(f.field_key, label) return { key: f.field_key, label, type: f.data_type, slots: f.slots ?? def?.slots, otherSlot: f.otherSlot ?? def?.otherSlot, // A select shows its option label ("In use"), not its value ("in_use"). value: optionLabel(def, formRow[f.field_key]), } }) const code = assetCode(config, row) const title = code || 'Asset' const subtitle = assetInitValue(config, row, 'Model') // Newest first, like the Asset History page. const history = assetEvents(config, row) .reverse() .map((e) => ({ ...e, title: e.employee ? `${e.action} · ${e.employee}` : e.action, meta: e.meta || undefined, })) const state = instanceState(row) const isAssigned = isAssetState(row, ASSET_STATE.ASSIGNED) const isAvailable = isAssetState(row, ASSET_STATE.AVAILABLE) const isUnderRepair = isAssetState(row, ASSET_STATE.UNDER_REPAIR) // ASSIGNED and Send for Repair run in Available; Reassigned in Assigned; Lost // in either; Repair Completed in Under Repair; Edit in all three. RETURNED // runs in any state but Closed, and the runtime says if the state doesn't // allow it. If the view sends no state, let the runtime decide rather than // hiding the buttons. const canAssign = !state || isAvailable const canReassign = !state || isAssigned const canReturn = !isAssetState(row, ASSET_STATE.CLOSED) && !isEndState(row) const canReportLost = !state || isAvailable || isAssigned const canSendForRepair = !state || isAvailable const canCompleteRepair = !state || isUnderRepair const canEdit = !state || isAvailable || isAssigned || isUnderRepair // Assigned or reassigned, whichever is newer, is who has it now. const current = currentAssignment(row) const employee = current.name // Photos show like the asset's own: captioned by slot (Front, Back…) and // found by field id or by the view's column for them. Other values go by id // only — several activities share labels like "Remarks". const toFields = (defs) => defs.map((f) => ({ key: f.id, label: f.formLabel ?? f.label, type: f.type, slots: f.slots, otherSlot: f.otherSlot, value: f.type === 'image' ? rowFieldValue(f, row) : row[f.id], })) const assignmentFields = [ { key: ASSIGN_EMPLOYEE_FIELD, label: 'Employee data', type: 'text', value: lookupName(row, ASSIGN_EMPLOYEE_FIELD) }, ...toFields(ASSET_ASSIGN_FIELDS), ] const reassignmentFields = [ { key: REASSIGN_EMPLOYEE_FIELD, label: 'Employee data', type: 'text', value: lookupName(row, REASSIGN_EMPLOYEE_FIELD) }, ...toFields(ASSET_REASSIGN_FIELDS), ] const returnFields = [ { key: RETURN_EMPLOYEE_FIELD, label: 'Returned by', type: 'text', value: lookupName(row, RETURN_EMPLOYEE_FIELD) }, ...toFields(ASSET_RETURN_FIELDS), ] const repairFields = [...toFields(ASSET_REPAIR_FIELDS), ...toFields(ASSET_REPAIRED_FIELDS)] const lostFields = [ { key: LOST_EMPLOYEE_FIELD, label: 'Last owner', type: 'text', value: lookupName(row, LOST_EMPLOYEE_FIELD) }, ...toFields(ASSET_LOST_FIELDS), ] const hasValues = (fields) => fields.some((f) => f.value != null && f.value !== '') const meta = Object.keys(row) .filter((k) => META_KEY.test(k) && row[k] != null && row[k] !== '' && typeof row[k] !== 'object') .map((k) => ({ key: k, label: k.replace(/_/g, ' '), value: row[k] })) function openActivity(kind) { setNotice(null) setActivity(kind) } /** `emp` is the employee picked in the dialog; null when Lost ran without one. */ function handleDone(emp) { const text = { assign: `Asset assigned to ${emp?.name}.`, reassign: `Asset reassigned to ${emp?.name}.`, return: `Asset returned by ${emp?.name}.`, lost: 'Asset was reported lost and closed.', edit: 'Asset details saved.', repair: 'Asset sent for repair.', repaired: 'Repair completed. The asset is available again.', }[activity] setNotice({ tone: 'success', text }) load() } return ( <> Back {isUnderRepair ? ( ) : ( )} } /> {!canEdit && (
This asset is in "{state}", so its workflow has finished. It can't be assigned, edited, repaired, returned or reported lost any more.
)} {notice && (
{notice.text}
)}
{/* ---------- Left: state + record info ---------- */}
{state ? : null} {/* After a return the old assignee stays on the record; it's not current. */} {isAssigned && employee ? employee : null} {history.length ? ( ) : ( )} {meta.length > 0 && ( {meta.map((m) => ( {formatMeta(m.value)} ))} )}
{/* ---------- Right: asset + assignment fields ---------- */}
{hasValues(assignmentFields) ? ( ) : ( )} {hasValues(reassignmentFields) && ( )} {hasValues(returnFields) && ( )} {hasValues(repairFields) && ( )} {hasValues(lostFields) && ( )}
{activity && ( setActivity(null)} onDone={handleDone} /> )} ) } function DetailFields({ fields }) { return (
{fields.map((f) => (
{displayLabel(f.label)}
))}
) }