feat: asset edit, repair flow, return photos and new INIT fields
- Return activity: "Return images" photo field (Front/Back/Screen/Keyboard + Other) uploaded via /upload, and a "Received by" dropdown (Praveen, Akshay, Prasad) - Add Asset (INIT): "Monthly allowance" and "Repair status" fields - Edit Asset activity: updates location, state, priority, condition, repair status, allowance and warranty; pre-filled from current values, and edited values replace INIT values in the list and detail page - Repair flow: new "Under Repair" state with Send for Repair and Repair Completed activities, a Repair card, history events and filters, and an Under repair tile and chart color on the dashboard - Activity list rows open the asset by assetRowId New builder field and activity ids are placeholders (marked PLACEHOLDER in api/assets.js) until they are created in the Zino builder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f845af5125
commit
d9f614857a
@ -44,6 +44,21 @@ export const ASSET_INIT_ACTIVITY_ID = 'a2405547-0db4-474b-aa76-af1246d01a6a'
|
|||||||
*/
|
*/
|
||||||
export const ASSET_INVOICE_FIELD = 'field_62'
|
export const ASSET_INVOICE_FIELD = 'field_62'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The monthly allowance paid for the asset. PLACEHOLDER id: add a number field
|
||||||
|
* labelled "Monthly allowance" to the INIT activity in the Zino builder,
|
||||||
|
* deploy, and put its id here. Until then the runtime may reject it.
|
||||||
|
*/
|
||||||
|
export const ASSET_MONTHLY_ALLOWANCE_FIELD = 'field_65'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where the asset stands in the repair flow. PLACEHOLDER id: add a dropdown
|
||||||
|
* field labelled "Repair status" to the INIT activity in the Zino builder,
|
||||||
|
* with the option values below, deploy, and put its id here. Until then the
|
||||||
|
* runtime may reject it.
|
||||||
|
*/
|
||||||
|
export const ASSET_REPAIR_STATUS_FIELD = 'field_80'
|
||||||
|
|
||||||
export const ASSET_FIELDS = [
|
export const ASSET_FIELDS = [
|
||||||
{
|
{
|
||||||
// The asset table stores it in its asset_details column. formLabel also
|
// The asset table stores it in its asset_details column. formLabel also
|
||||||
@ -67,6 +82,7 @@ export const ASSET_FIELDS = [
|
|||||||
{ id: 'field_44', label: 'Serial number', type: 'text' },
|
{ id: 'field_44', label: 'Serial number', type: 'text' },
|
||||||
{ id: 'field_45', label: 'Purchase date', type: 'date' },
|
{ id: 'field_45', label: 'Purchase date', type: 'date' },
|
||||||
{ id: 'field_46', label: 'Purchase cost', type: 'number' },
|
{ id: 'field_46', label: 'Purchase cost', type: 'number' },
|
||||||
|
{ id: ASSET_MONTHLY_ALLOWANCE_FIELD, label: 'Monthly allowance', type: 'number' },
|
||||||
// Option values must match the builder exactly.
|
// Option values must match the builder exactly.
|
||||||
{
|
{
|
||||||
id: 'field_56',
|
id: 'field_56',
|
||||||
@ -89,6 +105,17 @@ export const ASSET_FIELDS = [
|
|||||||
{ label: 'High', value: 'high' },
|
{ label: 'High', value: 'high' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: ASSET_REPAIR_STATUS_FIELD,
|
||||||
|
label: 'Repair status',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'Not required', value: 'not_required' },
|
||||||
|
{ label: 'Needs repair', value: 'needs_repair' },
|
||||||
|
{ label: 'Under repair', value: 'under_repair' },
|
||||||
|
{ label: 'Repaired', value: 'repaired' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'field_47',
|
id: 'field_47',
|
||||||
label: 'Condition',
|
label: 'Condition',
|
||||||
@ -284,19 +311,54 @@ export const REASSIGN_EMPLOYEE_FIELD = 'field_23'
|
|||||||
* POST {APP_API_BASE_URL}/app/{APP_ID}/activity
|
* POST {APP_API_BASE_URL}/app/{APP_ID}/activity
|
||||||
* Authorization: Bearer <JWT>
|
* Authorization: Bearer <JWT>
|
||||||
* { activity_id, workflow_uuid, instance_id,
|
* { activity_id, workflow_uuid, instance_id,
|
||||||
* data: { field_8, field_9, field_10, field_28: { id, first_name } } }
|
* data: { field_8, field_9, field_10, field_28: { id, first_name },
|
||||||
|
* [ASSET_RETURN_IMAGES_FIELD]: [{ blob_path, mime_type, original_name, size_bytes, uuid }, …] } }
|
||||||
|
*
|
||||||
|
* Each return photo is uploaded first (POST /app/{APP_ID}/upload with this
|
||||||
|
* activity's id and the field id), and the activity sends the returned file
|
||||||
|
* objects, as the start/activity endpoints expect.
|
||||||
*
|
*
|
||||||
* Source: the asset workflow "RETURNED" activity, Fields tab, in the Zino
|
* Source: the asset workflow "RETURNED" activity, Fields tab, in the Zino
|
||||||
* builder (…/activities/7f7b43fb-…).
|
* builder (…/activities/7f7b43fb-…).
|
||||||
*/
|
*/
|
||||||
export const ASSET_RETURN_ACTIVITY_ID = '7f7b43fb-557c-483a-97a5-09b0a8080622'
|
export const ASSET_RETURN_ACTIVITY_ID = '7f7b43fb-557c-483a-97a5-09b0a8080622'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Photos of the laptop as it comes back. PLACEHOLDER id: add an image field
|
||||||
|
* labelled "Return images" to the RETURNED activity in the Zino builder,
|
||||||
|
* deploy, and put its id here. Until then, picking a photo fails to upload;
|
||||||
|
* returns without photos save as before.
|
||||||
|
*/
|
||||||
|
export const ASSET_RETURN_IMAGES_FIELD = 'field_63'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Who takes the asset back. PLACEHOLDER id: add a dropdown field labelled
|
||||||
|
* "Received by" to the RETURNED activity in the Zino builder, with the options
|
||||||
|
* below, deploy, and put its id here. Until then the runtime may reject it.
|
||||||
|
*/
|
||||||
|
export const ASSET_RETURN_RECEIVER_FIELD = 'field_64'
|
||||||
|
|
||||||
/** Fields the user fills in. */
|
/** Fields the user fills in. */
|
||||||
export const ASSET_RETURN_FIELDS = [
|
export const ASSET_RETURN_FIELDS = [
|
||||||
{ id: 'field_8', label: 'Returned date', type: 'date' },
|
{ id: 'field_8', label: 'Returned date', type: 'date' },
|
||||||
|
{
|
||||||
|
id: ASSET_RETURN_RECEIVER_FIELD,
|
||||||
|
label: 'Received by',
|
||||||
|
type: 'select',
|
||||||
|
options: ['Praveen', 'Akshay', 'Prasad'].map((name) => ({ label: name, value: name })),
|
||||||
|
},
|
||||||
// Condition (field_29) is left out of the form on purpose.
|
// Condition (field_29) is left out of the form on purpose.
|
||||||
{ id: 'field_9', label: 'Damage details', type: 'longtext' },
|
{ id: 'field_9', label: 'Damage details', type: 'longtext' },
|
||||||
{ id: 'field_10', label: 'Remarks', type: 'longtext' },
|
{ id: 'field_10', label: 'Remarks', type: 'longtext' },
|
||||||
|
// One photo per view, then any number of others; sent as one list in this order.
|
||||||
|
{
|
||||||
|
id: ASSET_RETURN_IMAGES_FIELD,
|
||||||
|
label: 'Return images',
|
||||||
|
type: 'image',
|
||||||
|
maxSizeMb: 5,
|
||||||
|
slots: ['Front', 'Back', 'Screen', 'Keyboard'],
|
||||||
|
otherSlot: 'Other',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
/** "Returned by": a lookup into the employee table, like "Employee data". */
|
/** "Returned by": a lookup into the employee table, like "Employee data". */
|
||||||
@ -414,10 +476,129 @@ export function reportAssetLost(instanceId, values, employee) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "Edit" activity of the asset workflow. It updates the details of an
|
||||||
|
* asset that change over time, without moving it to another state. It should
|
||||||
|
* be offered in Available, Assigned and Under Repair. Like ASSIGNED it posts
|
||||||
|
* to /activity with the asset's instance_id.
|
||||||
|
*
|
||||||
|
* POST {APP_API_BASE_URL}/app/{APP_ID}/activity
|
||||||
|
* { activity_id, workflow_uuid, instance_id, data: { field_66 … field_72, field_81 } }
|
||||||
|
*
|
||||||
|
* Like the employee Update activity, it has its own field ids; `initId` is the
|
||||||
|
* INIT field each one updates, so the asset shows the edited value in its
|
||||||
|
* place (see withEditedValues).
|
||||||
|
*
|
||||||
|
* PLACEHOLDER ids: add the "Edit" activity and its fields (same labels, types
|
||||||
|
* and options as the INIT fields) in the Zino builder, deploy, and put the
|
||||||
|
* activity id and field ids here. Until then, running it fails.
|
||||||
|
*/
|
||||||
|
export const ASSET_EDIT_ACTIVITY_ID = 'TODO-edit-activity-id'
|
||||||
|
|
||||||
|
/** A copy of an INIT field for the Edit activity, under the Edit field's id. */
|
||||||
|
const editFieldOf = (initId, id) => ({ ...ASSET_FIELDS.find((f) => f.id === initId), id, initId })
|
||||||
|
|
||||||
|
/** Fields the user fills in, pre-filled with the asset's current values. */
|
||||||
|
export const ASSET_EDIT_FIELDS = [
|
||||||
|
editFieldOf('field_61', 'field_66'), // Asset Location
|
||||||
|
editFieldOf('field_56', 'field_67'), // Asset State
|
||||||
|
editFieldOf('field_58', 'field_68'), // Asset Priority
|
||||||
|
editFieldOf('field_47', 'field_69'), // Condition
|
||||||
|
editFieldOf(ASSET_REPAIR_STATUS_FIELD, 'field_81'),
|
||||||
|
editFieldOf(ASSET_MONTHLY_ALLOWANCE_FIELD, 'field_70'),
|
||||||
|
editFieldOf('field_48', 'field_71'), // Warranty Status
|
||||||
|
editFieldOf('field_55', 'field_72'), // Warranty End date
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The repair flow: "Send for Repair" moves an Available asset to Under Repair,
|
||||||
|
* and "Repair Completed" moves it back to Available. Assigned assets are
|
||||||
|
* returned first, so the return records the damage. Both post to /activity
|
||||||
|
* with the asset's instance_id.
|
||||||
|
*
|
||||||
|
* POST {APP_API_BASE_URL}/app/{APP_ID}/activity
|
||||||
|
* { activity_id, workflow_uuid, instance_id, data: { field_73 … field_76 } }
|
||||||
|
* { activity_id, workflow_uuid, instance_id, data: { field_77 … field_79 } }
|
||||||
|
*
|
||||||
|
* PLACEHOLDER ids: add an "Under Repair" state and these two activities, with
|
||||||
|
* these fields, in the Zino builder, deploy, and put their ids here. Until
|
||||||
|
* then, running them fails.
|
||||||
|
*/
|
||||||
|
export const ASSET_REPAIR_ACTIVITY_ID = 'TODO-send-for-repair-activity-id'
|
||||||
|
|
||||||
|
/** Send for Repair fields. */
|
||||||
|
export const ASSET_REPAIR_FIELDS = [
|
||||||
|
{ id: 'field_73', label: 'Repair date', type: 'date' },
|
||||||
|
{ id: 'field_74', label: 'Issue details', type: 'longtext' },
|
||||||
|
{ id: 'field_75', label: 'Repair vendor', type: 'text' },
|
||||||
|
{ id: 'field_76', label: 'Estimated cost', type: 'number' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const ASSET_REPAIRED_ACTIVITY_ID = 'TODO-repair-completed-activity-id'
|
||||||
|
|
||||||
|
/** Repair Completed fields. */
|
||||||
|
export const ASSET_REPAIRED_FIELDS = [
|
||||||
|
{ id: 'field_77', label: 'Repaired date', type: 'date' },
|
||||||
|
{ id: 'field_78', label: 'Repair cost', type: 'number' },
|
||||||
|
{ id: 'field_79', label: 'Repair remarks', type: 'longtext' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/** Run the Edit activity on an asset instance. */
|
||||||
|
export function editAsset(instanceId, values) {
|
||||||
|
return runAssetActivity(ASSET_EDIT_ACTIVITY_ID, ASSET_EDIT_FIELDS, instanceId, values)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Run Send for Repair on an Available asset instance. */
|
||||||
|
export function sendAssetForRepair(instanceId, values) {
|
||||||
|
return runAssetActivity(ASSET_REPAIR_ACTIVITY_ID, ASSET_REPAIR_FIELDS, instanceId, values)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Run Repair Completed on an asset instance that is Under Repair. */
|
||||||
|
export function completeAssetRepair(instanceId, values) {
|
||||||
|
return runAssetActivity(ASSET_REPAIRED_ACTIVITY_ID, ASSET_REPAIRED_FIELDS, instanceId, values)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Edit form's starting values: each field's current value on the asset
|
||||||
|
* (its latest edit, else its INIT value), as the form holds it.
|
||||||
|
*/
|
||||||
|
export function assetEditValues(row) {
|
||||||
|
const norm = (v) => String(v).trim().toLowerCase()
|
||||||
|
return Object.fromEntries(
|
||||||
|
ASSET_EDIT_FIELDS.map((f) => {
|
||||||
|
const init = ASSET_FIELDS.find((d) => d.id === f.initId)
|
||||||
|
const raw = present(row?.[f.id]) ? row[f.id] : row?.[initValueKey(init, row ?? {})]
|
||||||
|
if (!present(raw)) return [f.id, '']
|
||||||
|
if (f.type === 'date') return [f.id, toISODate(raw)]
|
||||||
|
if (f.type === 'select') {
|
||||||
|
const hit = f.options.find((o) => norm(o.value) === norm(raw) || norm(o.label) === norm(raw))
|
||||||
|
return [f.id, hit ? hit.value : String(raw)]
|
||||||
|
}
|
||||||
|
return [f.id, String(raw)]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A row with each edited value in place of its INIT value, so the asset shows
|
||||||
|
* its latest details. The INIT value stays when the asset was never edited.
|
||||||
|
*/
|
||||||
|
export function withEditedValues(row) {
|
||||||
|
if (!row) return row
|
||||||
|
const out = { ...row }
|
||||||
|
for (const f of ASSET_EDIT_FIELDS) {
|
||||||
|
if (!present(row[f.id])) continue
|
||||||
|
const init = ASSET_FIELDS.find((d) => d.id === f.initId)
|
||||||
|
out[initValueKey(init, row)] = row[f.id]
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
/** Asset workflow states, as named in the builder diagram. */
|
/** Asset workflow states, as named in the builder diagram. */
|
||||||
export const ASSET_STATE = {
|
export const ASSET_STATE = {
|
||||||
AVAILABLE: 'Available',
|
AVAILABLE: 'Available',
|
||||||
ASSIGNED: 'Assigned',
|
ASSIGNED: 'Assigned',
|
||||||
|
UNDER_REPAIR: 'Under Repair',
|
||||||
CLOSED: 'Closed',
|
CLOSED: 'Closed',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -662,11 +843,21 @@ export const ASSET_RICH_COLUMNS = [
|
|||||||
key: 'status',
|
key: 'status',
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
title: ['Asset State'],
|
title: ['Asset State'],
|
||||||
subtitle: [{ label: 'Asset Priority', prefix: 'Priority' }, 'Condition', 'Asset Classification'],
|
subtitle: [
|
||||||
|
{ label: 'Asset Priority', prefix: 'Priority' },
|
||||||
|
'Condition',
|
||||||
|
{ label: 'Repair status', prefix: 'Repair' },
|
||||||
|
'Asset Classification',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{ key: 'owner', label: 'Owner', title: ['Asset Owner'], subtitle: ['Asset Location'] },
|
{ key: 'owner', label: 'Owner', title: ['Asset Owner'], subtitle: ['Asset Location'] },
|
||||||
{ key: 'specs', label: 'Specs', title: ['CPU'], subtitle: ['Hard Drive'] },
|
{ key: 'specs', label: 'Specs', title: ['CPU'], subtitle: ['Hard Drive'] },
|
||||||
{ key: 'purchase', label: 'Purchase', title: ['Purchase cost'], subtitle: ['Purchase date'] },
|
{
|
||||||
|
key: 'purchase',
|
||||||
|
label: 'Purchase',
|
||||||
|
title: ['Purchase cost'],
|
||||||
|
subtitle: ['Purchase date', { label: 'Monthly allowance', prefix: 'Allowance' }],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'warranty',
|
key: 'warranty',
|
||||||
label: 'Warranty',
|
label: 'Warranty',
|
||||||
@ -684,7 +875,8 @@ export const ASSET_RICH_COLUMNS = [
|
|||||||
*/
|
*/
|
||||||
export async function withAssetListDisplay(res) {
|
export async function withAssetListDisplay(res) {
|
||||||
const rows = Array.isArray(res?.data) ? res.data : []
|
const rows = Array.isArray(res?.data) ? res.data : []
|
||||||
const display = withAssetFormColumns({ ...res, data: await withOwnerNames(res?.config, rows) })
|
const named = await withOwnerNames(res?.config, rows.map(withEditedValues))
|
||||||
|
const display = withAssetFormColumns({ ...res, data: named })
|
||||||
return withRichColumns(display, ASSET_RICH_COLUMNS)
|
return withRichColumns(display, ASSET_RICH_COLUMNS)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -847,6 +1039,26 @@ export function assetEvents(config, row) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (present(val('field_73'))) {
|
||||||
|
add(
|
||||||
|
HISTORY_ACTION.REPAIR,
|
||||||
|
val('field_73'),
|
||||||
|
'',
|
||||||
|
joinParts(val('field_75'), present(val('field_76')) && `Estimate ${val('field_76')}`),
|
||||||
|
val('field_74'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (present(val('field_77'))) {
|
||||||
|
add(
|
||||||
|
HISTORY_ACTION.REPAIRED,
|
||||||
|
val('field_77'),
|
||||||
|
'',
|
||||||
|
joinParts(present(val('field_78')) && `Cost ${val('field_78')}`),
|
||||||
|
val('field_79'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const lastOwner = lookupName(row, LOST_EMPLOYEE_FIELD)
|
const lastOwner = lookupName(row, LOST_EMPLOYEE_FIELD)
|
||||||
if (present(val('field_36')) || lastOwner) {
|
if (present(val('field_36')) || lastOwner) {
|
||||||
add(
|
add(
|
||||||
@ -877,6 +1089,8 @@ export const HISTORY_FILTERS = [
|
|||||||
{ value: HISTORY_ACTION.ASSIGNED, label: 'Assigned' },
|
{ value: HISTORY_ACTION.ASSIGNED, label: 'Assigned' },
|
||||||
{ value: HISTORY_ACTION.REASSIGNED, label: 'Reassigned' },
|
{ value: HISTORY_ACTION.REASSIGNED, label: 'Reassigned' },
|
||||||
{ value: HISTORY_ACTION.RETURNED, label: 'Returned' },
|
{ value: HISTORY_ACTION.RETURNED, label: 'Returned' },
|
||||||
|
{ value: HISTORY_ACTION.REPAIR, label: 'Sent for repair' },
|
||||||
|
{ value: HISTORY_ACTION.REPAIRED, label: 'Repaired' },
|
||||||
{ value: HISTORY_ACTION.LOST, label: 'Lost' },
|
{ value: HISTORY_ACTION.LOST, label: 'Lost' },
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -963,6 +1177,6 @@ export async function fetchAssetInstance(instanceId) {
|
|||||||
const row = { ...detailValues }
|
const row = { ...detailValues }
|
||||||
for (const [k, v] of Object.entries(listRow ?? {})) if (present(v) || !(k in row)) row[k] = v
|
for (const [k, v] of Object.entries(listRow ?? {})) if (present(v) || !(k in row)) row[k] = v
|
||||||
|
|
||||||
const [named] = await withOwnerNames(config, [row])
|
const [named] = await withOwnerNames(config, [withEditedValues(row)])
|
||||||
return { config, row: named }
|
return { config, row: named }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,6 +47,7 @@ export async function fetchDashboard() {
|
|||||||
const stateCounts = new Map([
|
const stateCounts = new Map([
|
||||||
[ASSET_STATE.ASSIGNED, 0],
|
[ASSET_STATE.ASSIGNED, 0],
|
||||||
[ASSET_STATE.AVAILABLE, 0],
|
[ASSET_STATE.AVAILABLE, 0],
|
||||||
|
[ASSET_STATE.UNDER_REPAIR, 0],
|
||||||
[ASSET_STATE.CLOSED, 0],
|
[ASSET_STATE.CLOSED, 0],
|
||||||
])
|
])
|
||||||
const categoryCounts = new Map()
|
const categoryCounts = new Map()
|
||||||
|
|||||||
@ -20,6 +20,8 @@ const PATHS = {
|
|||||||
building: 'M3 21h18M5 21V7l7-4 7 4v14M9 9h.01M9 13h.01M9 17h.01M15 9h.01M15 13h.01M15 17h.01',
|
building: 'M3 21h18M5 21V7l7-4 7 4v14M9 9h.01M9 13h.01M9 17h.01M15 9h.01M15 13h.01M15 17h.01',
|
||||||
tag: 'M20.59 13.41 13.42 20.6a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82zM7 7h.01',
|
tag: 'M20.59 13.41 13.42 20.6a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82zM7 7h.01',
|
||||||
transfer: 'M16 3h5v5M21 3l-7 7M8 21H3v-5M3 21l7-7',
|
transfer: 'M16 3h5v5M21 3l-7 7M8 21H3v-5M3 21l7-7',
|
||||||
|
edit: 'M12 20h9M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z',
|
||||||
|
repair: 'M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z',
|
||||||
laptop: 'M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v11H3V5zM2 20h20',
|
laptop: 'M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v11H3V5zM2 20h20',
|
||||||
logout: 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9',
|
logout: 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export const ASSET_STATUS_TONE = {
|
|||||||
export const ASSET_STATE_TONE = {
|
export const ASSET_STATE_TONE = {
|
||||||
available: 'success',
|
available: 'success',
|
||||||
assigned: 'info',
|
assigned: 'info',
|
||||||
|
'under repair': 'warning',
|
||||||
closed: 'neutral',
|
closed: 'neutral',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,6 +117,8 @@ export const HISTORY_ACTION = {
|
|||||||
RETURNED: 'Returned',
|
RETURNED: 'Returned',
|
||||||
TRANSFERRED: 'Transferred',
|
TRANSFERRED: 'Transferred',
|
||||||
MAINTENANCE: 'Maintenance',
|
MAINTENANCE: 'Maintenance',
|
||||||
|
REPAIR: 'Sent for repair',
|
||||||
|
REPAIRED: 'Repaired',
|
||||||
LOST: 'Lost',
|
LOST: 'Lost',
|
||||||
RETIRED: 'Retired',
|
RETIRED: 'Retired',
|
||||||
}
|
}
|
||||||
@ -130,5 +133,7 @@ export const HISTORY_ACTION_TONE = {
|
|||||||
[HISTORY_ACTION.RETURNED]: 'neutral',
|
[HISTORY_ACTION.RETURNED]: 'neutral',
|
||||||
[HISTORY_ACTION.TRANSFERRED]: 'warning',
|
[HISTORY_ACTION.TRANSFERRED]: 'warning',
|
||||||
[HISTORY_ACTION.MAINTENANCE]: 'warning',
|
[HISTORY_ACTION.MAINTENANCE]: 'warning',
|
||||||
|
[HISTORY_ACTION.REPAIR]: 'warning',
|
||||||
|
[HISTORY_ACTION.REPAIRED]: 'success',
|
||||||
[HISTORY_ACTION.RETIRED]: 'danger',
|
[HISTORY_ACTION.RETIRED]: 'danger',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { formatCurrency } from '../utils/format'
|
|||||||
const STATE_COLOR = {
|
const STATE_COLOR = {
|
||||||
[ASSET_STATE.ASSIGNED]: 'var(--series-1)',
|
[ASSET_STATE.ASSIGNED]: 'var(--series-1)',
|
||||||
[ASSET_STATE.AVAILABLE]: 'var(--series-3)',
|
[ASSET_STATE.AVAILABLE]: 'var(--series-3)',
|
||||||
|
[ASSET_STATE.UNDER_REPAIR]: 'var(--series-4)',
|
||||||
[ASSET_STATE.CLOSED]: 'var(--series-2)',
|
[ASSET_STATE.CLOSED]: 'var(--series-2)',
|
||||||
}
|
}
|
||||||
const OTHER_COLOR = 'var(--border-strong)'
|
const OTHER_COLOR = 'var(--border-strong)'
|
||||||
@ -106,6 +107,7 @@ export default function Dashboard() {
|
|||||||
<StatTile label="Total assets" value={data.total} to="/assets" />
|
<StatTile label="Total assets" value={data.total} to="/assets" />
|
||||||
<StatTile label="Assigned" value={count(ASSET_STATE.ASSIGNED)} to="/assign" />
|
<StatTile label="Assigned" value={count(ASSET_STATE.ASSIGNED)} to="/assign" />
|
||||||
<StatTile label="Available" value={count(ASSET_STATE.AVAILABLE)} to="/assign" />
|
<StatTile label="Available" value={count(ASSET_STATE.AVAILABLE)} to="/assign" />
|
||||||
|
<StatTile label="Under repair" value={count(ASSET_STATE.UNDER_REPAIR)} to="/assets" />
|
||||||
<StatTile label="Closed (lost or retired)" value={count(ASSET_STATE.CLOSED)} to="/history" />
|
<StatTile label="Closed (lost or retired)" value={count(ASSET_STATE.CLOSED)} to="/history" />
|
||||||
<StatTile
|
<StatTile
|
||||||
label="Active employees"
|
label="Active employees"
|
||||||
|
|||||||
@ -7,11 +7,12 @@ import {
|
|||||||
ASSIGNMENT_FILTERS,
|
ASSIGNMENT_FILTERS,
|
||||||
HISTORY_FILTERS,
|
HISTORY_FILTERS,
|
||||||
RETURN_FILTERS,
|
RETURN_FILTERS,
|
||||||
|
assetRowId,
|
||||||
fetchAssignmentListPage,
|
fetchAssignmentListPage,
|
||||||
fetchHistoryListPage,
|
fetchHistoryListPage,
|
||||||
fetchReturnListPage,
|
fetchReturnListPage,
|
||||||
} from '../../api/assets'
|
} from '../../api/assets'
|
||||||
|
|
||||||
const VIEW = RECORD_VIEWS.assets
|
const VIEW = RECORD_VIEWS.assets
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -73,7 +74,7 @@ export default function AssetActivityList({ kind }) {
|
|||||||
{...view}
|
{...view}
|
||||||
rowKey={list.rowKey ?? VIEW.rowKey}
|
rowKey={list.rowKey ?? VIEW.rowKey}
|
||||||
emptyText={list.empty[view.query.filter] ?? 'No assets yet.'}
|
emptyText={list.empty[view.query.filter] ?? 'No assets yet.'}
|
||||||
onRowClick={(row) => navigate(`/assets/${encodeURIComponent(row.instance_id)}`)}
|
onRowClick={(row) => navigate(`/assets/${encodeURIComponent(assetRowId(row))}`)}
|
||||||
toolbar={
|
toolbar={
|
||||||
<div className="filter">
|
<div className="filter">
|
||||||
<SelectInput
|
<SelectInput
|
||||||
|
|||||||
@ -1,17 +1,25 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Button, Field, Icon, Modal, SelectInput } from '../../components/ui'
|
import { Button, Field, Icon, Modal, SelectInput } from '../../components/ui'
|
||||||
import ActivityFields from '../../components/activity/ActivityFields'
|
import ActivityFields from '../../components/activity/ActivityFields'
|
||||||
import {
|
import {
|
||||||
ASSET_ASSIGN_FIELDS,
|
ASSET_ASSIGN_FIELDS,
|
||||||
|
ASSET_EDIT_FIELDS,
|
||||||
ASSET_LOST_FIELDS,
|
ASSET_LOST_FIELDS,
|
||||||
ASSET_REASSIGN_FIELDS,
|
ASSET_REASSIGN_FIELDS,
|
||||||
|
ASSET_REPAIRED_FIELDS,
|
||||||
|
ASSET_REPAIR_FIELDS,
|
||||||
|
ASSET_RETURN_ACTIVITY_ID,
|
||||||
ASSET_RETURN_FIELDS,
|
ASSET_RETURN_FIELDS,
|
||||||
assignAsset,
|
assignAsset,
|
||||||
|
completeAssetRepair,
|
||||||
|
editAsset,
|
||||||
fetchAssignableEmployees,
|
fetchAssignableEmployees,
|
||||||
reassignAsset,
|
reassignAsset,
|
||||||
reportAssetLost,
|
reportAssetLost,
|
||||||
returnAsset,
|
returnAsset,
|
||||||
|
sendAssetForRepair,
|
||||||
|
uploadAssetFile,
|
||||||
} from '../../api/assets'
|
} from '../../api/assets'
|
||||||
import { emptyValues, validateActivity } from '../../api/activityForm'
|
import { emptyValues, validateActivity } from '../../api/activityForm'
|
||||||
import { useAuth } from '../../store/AuthContext'
|
import { useAuth } from '../../store/AuthContext'
|
||||||
@ -20,8 +28,9 @@ import { useAuth } from '../../store/AuthContext'
|
|||||||
* The asset activities that run against one existing asset and carry an
|
* The asset activities that run against one existing asset and carry an
|
||||||
* employee lookup. The employee is picked from a list of active employees; the
|
* employee lookup. The employee is picked from a list of active employees; the
|
||||||
* other fields come from api/assets.js, so each form matches its activity.
|
* other fields come from api/assets.js, so each form matches its activity.
|
||||||
* `employeeRequired: false` lets the form run without one; `warning` is shown
|
* `employeeRequired: false` lets the form run without one, and `employee: false`
|
||||||
* above the form for an activity that can't be undone.
|
* leaves the picker out; `warning` is shown above the form for an activity
|
||||||
|
* that can't be undone.
|
||||||
*/
|
*/
|
||||||
const ACTIVITIES = {
|
const ACTIVITIES = {
|
||||||
assign: {
|
assign: {
|
||||||
@ -49,6 +58,8 @@ const ACTIVITIES = {
|
|||||||
return: {
|
return: {
|
||||||
title: 'Return Asset',
|
title: 'Return Asset',
|
||||||
subtitle: 'Runs the RETURNED activity in the asset workflow.',
|
subtitle: 'Runs the RETURNED activity in the asset workflow.',
|
||||||
|
// Photo uploads are tagged with this activity's id.
|
||||||
|
activityId: ASSET_RETURN_ACTIVITY_ID,
|
||||||
fields: ASSET_RETURN_FIELDS,
|
fields: ASSET_RETURN_FIELDS,
|
||||||
employeeLabel: 'Returned by',
|
employeeLabel: 'Returned by',
|
||||||
employeeError: 'Choose who returned this asset.',
|
employeeError: 'Choose who returned this asset.',
|
||||||
@ -72,22 +83,55 @@ const ACTIVITIES = {
|
|||||||
failed: 'Could not report the asset lost. Please try again.',
|
failed: 'Could not report the asset lost. Please try again.',
|
||||||
run: reportAssetLost,
|
run: reportAssetLost,
|
||||||
},
|
},
|
||||||
|
edit: {
|
||||||
|
title: 'Edit Asset',
|
||||||
|
subtitle: 'Runs the Edit activity in the asset workflow. The asset keeps its state.',
|
||||||
|
fields: ASSET_EDIT_FIELDS,
|
||||||
|
employee: false,
|
||||||
|
submit: 'Save Changes',
|
||||||
|
submitting: 'Saving…',
|
||||||
|
failed: 'Could not save the changes. Please try again.',
|
||||||
|
run: editAsset,
|
||||||
|
},
|
||||||
|
repair: {
|
||||||
|
title: 'Send for Repair',
|
||||||
|
subtitle: 'Runs the Send for Repair activity. The asset moves to Under Repair.',
|
||||||
|
fields: ASSET_REPAIR_FIELDS,
|
||||||
|
employee: false,
|
||||||
|
submit: 'Send for Repair',
|
||||||
|
submitting: 'Sending…',
|
||||||
|
failed: 'Could not send the asset for repair. Please try again.',
|
||||||
|
run: sendAssetForRepair,
|
||||||
|
},
|
||||||
|
repaired: {
|
||||||
|
title: 'Repair Completed',
|
||||||
|
subtitle: 'Runs the Repair Completed activity. The asset is Available again.',
|
||||||
|
fields: ASSET_REPAIRED_FIELDS,
|
||||||
|
employee: false,
|
||||||
|
submit: 'Complete Repair',
|
||||||
|
submitting: 'Saving…',
|
||||||
|
failed: 'Could not complete the repair. Please try again.',
|
||||||
|
run: completeAssetRepair,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog for one asset activity. `kind` is a key of ACTIVITIES;
|
* Dialog for one asset activity. `kind` is a key of ACTIVITIES;
|
||||||
* `defaultEmployeeId` preselects an employee (e.g. the assignee on return).
|
* `defaultEmployeeId` preselects an employee (e.g. the assignee on return);
|
||||||
|
* `initialValues` pre-fills the fields (e.g. the asset's details on edit).
|
||||||
*/
|
*/
|
||||||
export default function AssetActivityModal({
|
export default function AssetActivityModal({
|
||||||
kind,
|
kind,
|
||||||
open,
|
open,
|
||||||
instanceId,
|
instanceId,
|
||||||
defaultEmployeeId,
|
defaultEmployeeId,
|
||||||
|
initialValues,
|
||||||
onClose,
|
onClose,
|
||||||
onDone,
|
onDone,
|
||||||
}) {
|
}) {
|
||||||
const activity = ACTIVITIES[kind]
|
const activity = ACTIVITIES[kind]
|
||||||
const employeeRequired = activity.employeeRequired !== false
|
const hasEmployee = activity.employee !== false
|
||||||
|
const employeeRequired = hasEmployee && activity.employeeRequired !== false
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { logout } = useAuth()
|
const { logout } = useAuth()
|
||||||
|
|
||||||
@ -98,15 +142,19 @@ export default function AssetActivityModal({
|
|||||||
const [errors, setErrors] = useState({})
|
const [errors, setErrors] = useState({})
|
||||||
const [serverError, setServerError] = useState('')
|
const [serverError, setServerError] = useState('')
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
// Read when the dialog opens; a new object on each parent render must not reset the form.
|
||||||
|
const initial = useRef(initialValues)
|
||||||
|
initial.current = initialValues
|
||||||
|
|
||||||
// Start from a blank form and a fresh employee list each time the dialog opens.
|
// Start from a fresh form and employee list each time the dialog opens.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return undefined
|
if (!open) return undefined
|
||||||
setEmployeeId(defaultEmployeeId != null ? String(defaultEmployeeId) : '')
|
setEmployeeId(defaultEmployeeId != null ? String(defaultEmployeeId) : '')
|
||||||
setValues(emptyValues(activity.fields))
|
setValues({ ...emptyValues(activity.fields), ...initial.current })
|
||||||
setErrors({})
|
setErrors({})
|
||||||
setServerError('')
|
setServerError('')
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
|
if (!hasEmployee) return undefined
|
||||||
|
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
setLoadingEmployees(true)
|
setLoadingEmployees(true)
|
||||||
@ -125,7 +173,7 @@ export default function AssetActivityModal({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
}, [open, activity, defaultEmployeeId, logout, navigate])
|
}, [open, activity, hasEmployee, defaultEmployeeId, logout, navigate])
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
if (submitting) return
|
if (submitting) return
|
||||||
@ -212,27 +260,33 @@ export default function AssetActivityModal({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<form id={formId} className="form__grid" onSubmit={handleSubmit} noValidate>
|
<form id={formId} className="form__grid" onSubmit={handleSubmit} noValidate>
|
||||||
<Field label={activity.employeeLabel} required={employeeRequired} error={errors.employee}>
|
{hasEmployee && (
|
||||||
{(id) => (
|
<Field label={activity.employeeLabel} required={employeeRequired} error={errors.employee}>
|
||||||
<SelectInput
|
{(id) => (
|
||||||
id={id}
|
<SelectInput
|
||||||
value={employeeId}
|
id={id}
|
||||||
onChange={(e) => {
|
value={employeeId}
|
||||||
setEmployeeId(e.target.value)
|
onChange={(e) => {
|
||||||
if (errors.employee) setErrors((er) => ({ ...er, employee: undefined }))
|
setEmployeeId(e.target.value)
|
||||||
}}
|
if (errors.employee) setErrors((er) => ({ ...er, employee: undefined }))
|
||||||
options={employeeOptions}
|
}}
|
||||||
placeholder={loadingEmployees ? 'Loading employees…' : 'Select employee'}
|
options={employeeOptions}
|
||||||
disabled={submitting || loadingEmployees}
|
placeholder={loadingEmployees ? 'Loading employees…' : 'Select employee'}
|
||||||
/>
|
disabled={submitting || loadingEmployees}
|
||||||
)}
|
/>
|
||||||
</Field>
|
)}
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
<ActivityFields
|
<ActivityFields
|
||||||
fields={activity.fields}
|
fields={activity.fields}
|
||||||
values={values}
|
values={values}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
|
upload={
|
||||||
|
activity.activityId &&
|
||||||
|
((fieldId, file) => uploadAssetFile(activity.activityId, fieldId, file))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@ -19,6 +19,8 @@ import {
|
|||||||
ASSET_ASSIGN_FIELDS,
|
ASSET_ASSIGN_FIELDS,
|
||||||
ASSET_LOST_FIELDS,
|
ASSET_LOST_FIELDS,
|
||||||
ASSET_REASSIGN_FIELDS,
|
ASSET_REASSIGN_FIELDS,
|
||||||
|
ASSET_REPAIRED_FIELDS,
|
||||||
|
ASSET_REPAIR_FIELDS,
|
||||||
ASSET_RETURN_FIELDS,
|
ASSET_RETURN_FIELDS,
|
||||||
ASSET_STATE,
|
ASSET_STATE,
|
||||||
ASSIGN_EMPLOYEE_FIELD,
|
ASSIGN_EMPLOYEE_FIELD,
|
||||||
@ -26,6 +28,7 @@ import {
|
|||||||
REASSIGN_EMPLOYEE_FIELD,
|
REASSIGN_EMPLOYEE_FIELD,
|
||||||
RETURN_EMPLOYEE_FIELD,
|
RETURN_EMPLOYEE_FIELD,
|
||||||
assetCode,
|
assetCode,
|
||||||
|
assetEditValues,
|
||||||
assetEvents,
|
assetEvents,
|
||||||
assetFieldDef,
|
assetFieldDef,
|
||||||
assetInitValue,
|
assetInitValue,
|
||||||
@ -56,7 +59,7 @@ export default function AssetDetails() {
|
|||||||
const [data, setData] = useState(null)
|
const [data, setData] = useState(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
// The activity dialog that's open: 'assign', 'reassign', 'return', 'lost' or null.
|
// The activity dialog that's open: a key of AssetActivityModal's ACTIVITIES, or null.
|
||||||
const [activity, setActivity] = useState(null)
|
const [activity, setActivity] = useState(null)
|
||||||
const [notice, setNotice] = useState(null)
|
const [notice, setNotice] = useState(null)
|
||||||
|
|
||||||
@ -163,13 +166,18 @@ export default function AssetDetails() {
|
|||||||
const state = instanceState(row)
|
const state = instanceState(row)
|
||||||
const isAssigned = isAssetState(row, ASSET_STATE.ASSIGNED)
|
const isAssigned = isAssetState(row, ASSET_STATE.ASSIGNED)
|
||||||
const isAvailable = isAssetState(row, ASSET_STATE.AVAILABLE)
|
const isAvailable = isAssetState(row, ASSET_STATE.AVAILABLE)
|
||||||
// ASSIGNED runs in Available; Reassigned and RETURNED in Assigned; Lost in
|
const isUnderRepair = isAssetState(row, ASSET_STATE.UNDER_REPAIR)
|
||||||
// either. If the view sends no state, let the runtime decide rather than
|
// ASSIGNED and Send for Repair run in Available; Reassigned and RETURNED in
|
||||||
|
// Assigned; Lost in either; Repair Completed in Under Repair; Edit in all
|
||||||
|
// three. If the view sends no state, let the runtime decide rather than
|
||||||
// hiding the buttons.
|
// hiding the buttons.
|
||||||
const canAssign = !state || isAvailable
|
const canAssign = !state || isAvailable
|
||||||
const canReassign = !state || isAssigned
|
const canReassign = !state || isAssigned
|
||||||
const canReturn = !state || isAssigned
|
const canReturn = !state || isAssigned
|
||||||
const canReportLost = !state || isAvailable || isAssigned
|
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.
|
// Assigned or reassigned, whichever is newer, is who has it now.
|
||||||
const current = currentAssignment(row)
|
const current = currentAssignment(row)
|
||||||
@ -188,6 +196,7 @@ export default function AssetDetails() {
|
|||||||
{ key: RETURN_EMPLOYEE_FIELD, label: 'Returned by', type: 'text', value: lookupName(row, RETURN_EMPLOYEE_FIELD) },
|
{ key: RETURN_EMPLOYEE_FIELD, label: 'Returned by', type: 'text', value: lookupName(row, RETURN_EMPLOYEE_FIELD) },
|
||||||
...toFields(ASSET_RETURN_FIELDS),
|
...toFields(ASSET_RETURN_FIELDS),
|
||||||
]
|
]
|
||||||
|
const repairFields = [...toFields(ASSET_REPAIR_FIELDS), ...toFields(ASSET_REPAIRED_FIELDS)]
|
||||||
const lostFields = [
|
const lostFields = [
|
||||||
{ key: LOST_EMPLOYEE_FIELD, label: 'Last owner', type: 'text', value: lookupName(row, LOST_EMPLOYEE_FIELD) },
|
{ key: LOST_EMPLOYEE_FIELD, label: 'Last owner', type: 'text', value: lookupName(row, LOST_EMPLOYEE_FIELD) },
|
||||||
...toFields(ASSET_LOST_FIELDS),
|
...toFields(ASSET_LOST_FIELDS),
|
||||||
@ -210,6 +219,9 @@ export default function AssetDetails() {
|
|||||||
reassign: `Asset reassigned to ${emp?.name}.`,
|
reassign: `Asset reassigned to ${emp?.name}.`,
|
||||||
return: `Asset returned by ${emp?.name}.`,
|
return: `Asset returned by ${emp?.name}.`,
|
||||||
lost: 'Asset was reported lost and closed.',
|
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]
|
}[activity]
|
||||||
setNotice({ tone: 'success', text })
|
setNotice({ tone: 'success', text })
|
||||||
load()
|
load()
|
||||||
@ -227,6 +239,9 @@ export default function AssetDetails() {
|
|||||||
<Button icon="history" onClick={load} disabled={loading}>
|
<Button icon="history" onClick={load} disabled={loading}>
|
||||||
{loading ? 'Refreshing…' : 'Refresh'}
|
{loading ? 'Refreshing…' : 'Refresh'}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button icon="edit" onClick={() => openActivity('edit')} disabled={!canEdit}>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
icon="alert"
|
icon="alert"
|
||||||
@ -235,6 +250,15 @@ export default function AssetDetails() {
|
|||||||
>
|
>
|
||||||
Report Lost
|
Report Lost
|
||||||
</Button>
|
</Button>
|
||||||
|
{isUnderRepair ? (
|
||||||
|
<Button icon="check" onClick={() => openActivity('repaired')} disabled={!canCompleteRepair}>
|
||||||
|
Complete Repair
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button icon="repair" onClick={() => openActivity('repair')} disabled={!canSendForRepair}>
|
||||||
|
Send for Repair
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button icon="return" onClick={() => openActivity('return')} disabled={!canReturn}>
|
<Button icon="return" onClick={() => openActivity('return')} disabled={!canReturn}>
|
||||||
Return Asset
|
Return Asset
|
||||||
</Button>
|
</Button>
|
||||||
@ -253,12 +277,12 @@ export default function AssetDetails() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!canAssign && !canReturn && !canReportLost && (
|
{!canEdit && (
|
||||||
<div className="notice notice--warning" role="status">
|
<div className="notice notice--warning" role="status">
|
||||||
<Icon name="alert" size={15} />
|
<Icon name="alert" size={15} />
|
||||||
<span>
|
<span>
|
||||||
This asset is in "{state}", so its workflow has finished. It can't be assigned,
|
This asset is in "{state}", so its workflow has finished. It can't be assigned, edited,
|
||||||
returned or reported lost any more.
|
repaired, returned or reported lost any more.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -346,6 +370,12 @@ export default function AssetDetails() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{hasValues(repairFields) && (
|
||||||
|
<Card title={isUnderRepair ? 'Repair' : 'Last repair'} flush>
|
||||||
|
<DetailFields fields={repairFields} />
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{hasValues(lostFields) && (
|
{hasValues(lostFields) && (
|
||||||
<Card title="Loss report" flush>
|
<Card title="Loss report" flush>
|
||||||
<DetailFields fields={lostFields} />
|
<DetailFields fields={lostFields} />
|
||||||
@ -363,6 +393,7 @@ export default function AssetDetails() {
|
|||||||
defaultEmployeeId={
|
defaultEmployeeId={
|
||||||
(activity === 'return' || activity === 'lost') && isAssigned ? current.id : null
|
(activity === 'return' || activity === 'lost') && isAssigned ? current.id : null
|
||||||
}
|
}
|
||||||
|
initialValues={activity === 'edit' ? assetEditValues(row) : undefined}
|
||||||
onClose={() => setActivity(null)}
|
onClose={() => setActivity(null)}
|
||||||
onDone={handleDone}
|
onDone={handleDone}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1445,10 +1445,11 @@ button {
|
|||||||
|
|
||||||
/* ---------- Dashboard ---------- */
|
/* ---------- Dashboard ---------- */
|
||||||
.dash {
|
.dash {
|
||||||
/* Categorical slots 1–3, validated all-pairs on the white card surface. */
|
/* Categorical slots 1–4, validated on the white card surface. */
|
||||||
--series-1: #2a78d6;
|
--series-1: #2a78d6;
|
||||||
--series-2: #eb6834;
|
--series-2: #eb6834;
|
||||||
--series-3: #1baf7a;
|
--series-3: #1baf7a;
|
||||||
|
--series-4: #eda100;
|
||||||
transition: opacity 0.15s;
|
transition: opacity 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user