Compare commits
4 Commits
fix/asset-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5208e29905 | ||
|
|
25a635cda3 | ||
|
|
086c397f78 | ||
|
|
bd141e3585 |
@ -144,6 +144,8 @@ export const ASSET_FIELDS = [
|
|||||||
},
|
},
|
||||||
{ id: 'field_59', label: 'Asset Brand', type: 'text' },
|
{ id: 'field_59', label: 'Asset Brand', type: 'text' },
|
||||||
{ id: 'field_61', label: 'Asset Location', type: 'text' },
|
{ id: 'field_61', label: 'Asset Location', type: 'text' },
|
||||||
|
// `label` keeps the builder's spelling; `formLabel` is what the app shows.
|
||||||
|
{ id: 'field_73', label: 'Monthly allowence', formLabel: 'Monthly allowance', type: 'number' },
|
||||||
]
|
]
|
||||||
|
|
||||||
/** INIT's photo field, whose uploads the runtime always accepts. */
|
/** INIT's photo field, whose uploads the runtime always accepts. */
|
||||||
@ -190,8 +192,7 @@ export const fetchAssetActivityFieldDefs = (activityId) =>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* "Asset Owner": a lookup into the employee table, like ASSIGNED's "Employee
|
* "Asset Owner": a lookup into the employee table, like ASSIGNED's "Employee
|
||||||
* data". The API sample shows only { id }; it is sent as { id, first_name }
|
* data". Sent as { id, first_name } — see employeeLookup.
|
||||||
* because the runtime checks lookups against their storage columns.
|
|
||||||
*/
|
*/
|
||||||
export const ASSET_OWNER_FIELD = 'field_51'
|
export const ASSET_OWNER_FIELD = 'field_51'
|
||||||
|
|
||||||
@ -264,11 +265,9 @@ export const ASSET_ASSIGN_FIELDS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "Employee data": a lookup into the employee table. The runtime checks it
|
* "Employee data": a lookup into the employee table, sent as { id, first_name }
|
||||||
* against the lookup's storage columns (a Retired attempt failed with
|
* like every employee lookup (see employeeLookup). It is picked from a list
|
||||||
* "missing required storage column 'first_name'"), so it carries
|
* of active employees, so it is sent alongside the form data.
|
||||||
* { id, first_name }, not the { id, name } the API sample shows. It is picked
|
|
||||||
* from a list of active employees, so it is sent alongside the form data.
|
|
||||||
*/
|
*/
|
||||||
export const ASSIGN_EMPLOYEE_FIELD = 'field_22'
|
export const ASSIGN_EMPLOYEE_FIELD = 'field_22'
|
||||||
|
|
||||||
@ -439,8 +438,17 @@ export const ASSET_LOST_FIELDS = [
|
|||||||
/** "Last owner": a lookup into the employee table, like "Employee data". */
|
/** "Last owner": a lookup into the employee table, like "Employee data". */
|
||||||
export const LOST_EMPLOYEE_FIELD = 'field_35'
|
export const LOST_EMPLOYEE_FIELD = 'field_35'
|
||||||
|
|
||||||
/** An employee lookup value, in the shape the runtime validates. */
|
/**
|
||||||
const employeeLookup = (employee) => ({ id: employee.id, first_name: employee.first_name })
|
* An employee lookup value, in the shape the runtime validates: { id,
|
||||||
|
* first_name }, the lookup's storage columns. The employee table has no "name"
|
||||||
|
* column, so { id, name } fails with "… has an incomplete selection"
|
||||||
|
* (invalid_type), and omitting first_name fails with "missing required
|
||||||
|
* storage column 'first_name'".
|
||||||
|
*/
|
||||||
|
const employeeLookup = (employee) => ({
|
||||||
|
id: employee.id,
|
||||||
|
first_name: employee.first_name || employee.name,
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a later activity (ASSIGNED, RETURNED, …) on an existing asset instance.
|
* Run a later activity (ASSIGNED, RETURNED, …) on an existing asset instance.
|
||||||
@ -584,21 +592,23 @@ const EDITED_FIELDS = [
|
|||||||
* with the asset's instance_id.
|
* with the asset's instance_id.
|
||||||
*
|
*
|
||||||
* POST {APP_API_BASE_URL}/app/{APP_ID}/activity
|
* 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_80 … field_83 } }
|
||||||
* { activity_id, workflow_uuid, instance_id, data: { field_77 … field_79 } }
|
* { activity_id, workflow_uuid, instance_id, data: { field_77 … field_79 } }
|
||||||
*
|
*
|
||||||
* PLACEHOLDER ids: add an "Under Repair" state and these two activities, with
|
* 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
|
* these fields, in the Zino builder, deploy, and put their ids here. Until
|
||||||
* then, running them fails.
|
* then, running them fails. (The send fields were field_73 … field_76, but the
|
||||||
|
* builder has since given field_73 to INIT's "Monthly allowence", so they
|
||||||
|
* moved to field_80 …; match whatever ids the builder assigns when adding.)
|
||||||
*/
|
*/
|
||||||
export const ASSET_REPAIR_ACTIVITY_ID = 'TODO-send-for-repair-activity-id'
|
export const ASSET_REPAIR_ACTIVITY_ID = 'TODO-send-for-repair-activity-id'
|
||||||
|
|
||||||
/** Send for Repair fields. */
|
/** Send for Repair fields. */
|
||||||
export const ASSET_REPAIR_FIELDS = [
|
export const ASSET_REPAIR_FIELDS = [
|
||||||
{ id: 'field_73', label: 'Repair date', type: 'date' },
|
{ id: 'field_80', label: 'Repair date', type: 'date' },
|
||||||
{ id: 'field_74', label: 'Issue details', type: 'longtext' },
|
{ id: 'field_81', label: 'Issue details', type: 'longtext' },
|
||||||
{ id: 'field_75', label: 'Repair vendor', type: 'text' },
|
{ id: 'field_82', label: 'Repair vendor', type: 'text' },
|
||||||
{ id: 'field_76', label: 'Estimated cost', type: 'number' },
|
{ id: 'field_83', label: 'Estimated cost', type: 'number' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const ASSET_REPAIRED_ACTIVITY_ID = 'TODO-repair-completed-activity-id'
|
export const ASSET_REPAIRED_ACTIVITY_ID = 'TODO-repair-completed-activity-id'
|
||||||
@ -1209,7 +1219,7 @@ export const ASSET_RICH_COLUMNS = [
|
|||||||
title: ['Warranty Status'],
|
title: ['Warranty Status'],
|
||||||
subtitle: [
|
subtitle: [
|
||||||
{ label: 'Warranty Start date', prefix: 'From' },
|
{ label: 'Warranty Start date', prefix: 'From' },
|
||||||
{ label: 'Warranty End date', prefix: 'to' },
|
'Warranty End date',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -1670,13 +1680,13 @@ export function assetEvents(config, row) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (present(val('field_73'))) {
|
if (present(val('field_80'))) {
|
||||||
add(
|
add(
|
||||||
HISTORY_ACTION.REPAIR,
|
HISTORY_ACTION.REPAIR,
|
||||||
val('field_73'),
|
val('field_80'),
|
||||||
'',
|
'',
|
||||||
joinParts(val('field_75'), present(val('field_76')) && `Estimate ${val('field_76')}`),
|
joinParts(val('field_82'), present(val('field_83')) && `Estimate ${val('field_83')}`),
|
||||||
val('field_74'),
|
val('field_81'),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { useEffect, useState } from 'react'
|
|||||||
import {
|
import {
|
||||||
AssetStateBadge,
|
AssetStateBadge,
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
|
||||||
EmployeeStatusBadge,
|
EmployeeStatusBadge,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
HistoryActionBadge,
|
HistoryActionBadge,
|
||||||
@ -17,11 +16,16 @@ import { FileLinks, imageSources } from './FieldValue'
|
|||||||
/**
|
/**
|
||||||
* Renders a record view straight from the backend response: the columns are
|
* Renders a record view straight from the backend response: the columns are
|
||||||
* whatever `config.fields` returns, in that order, with the backend's own
|
* whatever `config.fields` returns, in that order, with the backend's own
|
||||||
* labels. Paging, sorting and search are handled by the server.
|
* labels. Sorting and search are handled by the server; instead of page
|
||||||
|
* buttons, a "Rows" selector sets how many records the server returns
|
||||||
|
* (search_query.limit), so picking 5 shows 5 rows and picking 100 shows 100.
|
||||||
* `toolbar` adds controls (e.g. a filter) next to the search box. A field with
|
* `toolbar` adds controls (e.g. a filter) next to the search box. A field with
|
||||||
* `sortable: false` can't be sorted. A rich column (see api/richColumns.js)
|
* `sortable: false` can't be sorted. A rich column (see api/richColumns.js)
|
||||||
* shows several fields in one cell and sorts by its `sort_key`.
|
* shows several fields in one cell and sorts by its `sort_key`.
|
||||||
*/
|
*/
|
||||||
|
/** How many rows the "Rows" selector can ask the server for. */
|
||||||
|
const ROW_OPTIONS = [5, 10, 25, 50, 100]
|
||||||
|
|
||||||
export default function RecordView({
|
export default function RecordView({
|
||||||
fields,
|
fields,
|
||||||
rows,
|
rows,
|
||||||
@ -29,7 +33,7 @@ export default function RecordView({
|
|||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
query,
|
query,
|
||||||
setPage,
|
setLimit,
|
||||||
setSearch,
|
setSearch,
|
||||||
setSort,
|
setSort,
|
||||||
rowKey = 'instance_id',
|
rowKey = 'instance_id',
|
||||||
@ -46,6 +50,8 @@ export default function RecordView({
|
|||||||
return () => clearTimeout(id)
|
return () => clearTimeout(id)
|
||||||
}, [term, query.search, setSearch])
|
}, [term, query.search, setSearch])
|
||||||
|
|
||||||
|
// Names the placeholder only: the backend searches its own columns whatever
|
||||||
|
// is_search says (it returns false on every field), so the box always shows.
|
||||||
// A rich column's fields stay searchable.
|
// A rich column's fields stay searchable.
|
||||||
const searchable = fields
|
const searchable = fields
|
||||||
.flatMap((f) => (f.data_type === 'rich' ? [...f.title, ...f.subtitle] : [f]))
|
.flatMap((f) => (f.data_type === 'rich' ? [...f.title, ...f.subtitle] : [f]))
|
||||||
@ -59,20 +65,20 @@ export default function RecordView({
|
|||||||
setSort(sortKey(field), dir)
|
setSort(sortKey(field), dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = pagination?.page ?? 1
|
|
||||||
const totalPages = pagination?.total_pages ?? 1
|
|
||||||
const total = pagination?.total_count ?? rows.length
|
const total = pagination?.total_count ?? rows.length
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toolbar count={total} countLabel="records">
|
<Toolbar count={total} countLabel="records">
|
||||||
{searchable.length > 0 && (
|
|
||||||
<SearchInput
|
<SearchInput
|
||||||
value={term}
|
value={term}
|
||||||
onChange={setTerm}
|
onChange={setTerm}
|
||||||
placeholder={`Search by ${searchable.map((f) => displayLabel(f.output_label)).join(', ')}…`}
|
placeholder={
|
||||||
|
searchable.length > 0
|
||||||
|
? `Search by ${searchable.map((f) => displayLabel(f.output_label)).join(', ')}…`
|
||||||
|
: 'Search…'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{toolbar}
|
{toolbar}
|
||||||
{loading && <span className="toolbar__count">Loading…</span>}
|
{loading && <span className="toolbar__count">Loading…</span>}
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
@ -139,26 +145,26 @@ export default function RecordView({
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{totalPages > 1 && (
|
{total > 0 && (
|
||||||
<div className="pager">
|
<div className="pager">
|
||||||
<Button
|
<label className="pager__label">
|
||||||
size="sm"
|
Rows{' '}
|
||||||
icon="chevronLeft"
|
<select
|
||||||
disabled={page <= 1 || loading}
|
className="pager__select"
|
||||||
onClick={() => setPage(page - 1)}
|
value={query.limit}
|
||||||
|
disabled={loading}
|
||||||
|
onChange={(e) => setLimit(Number(e.target.value))}
|
||||||
>
|
>
|
||||||
Previous
|
{ROW_OPTIONS.map((n) => (
|
||||||
</Button>
|
<option key={n} value={n}>
|
||||||
|
{n}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<span className="pager__label">
|
<span className="pager__label">
|
||||||
Page {page} of {totalPages}
|
Showing {Math.min(rows.length, total)} of {total} records
|
||||||
</span>
|
</span>
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
disabled={page >= totalPages || loading}
|
|
||||||
onClick={() => setPage(page + 1)}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -71,6 +71,7 @@ export default function useRecordView(
|
|||||||
error,
|
error,
|
||||||
query,
|
query,
|
||||||
setPage: (page) => setQuery((q) => ({ ...q, page })),
|
setPage: (page) => setQuery((q) => ({ ...q, page })),
|
||||||
|
setLimit: (limit) => setQuery((q) => ({ ...q, limit, page: 1 })),
|
||||||
setSearch: (search) => setQuery((q) => ({ ...q, search, page: 1 })),
|
setSearch: (search) => setQuery((q) => ({ ...q, search, page: 1 })),
|
||||||
setSort: (sortBy, sortDir) => setQuery((q) => ({ ...q, sortBy, sortDir, page: 1 })),
|
setSort: (sortBy, sortDir) => setQuery((q) => ({ ...q, sortBy, sortDir, page: 1 })),
|
||||||
setFilter: (filter) => setQuery((q) => ({ ...q, filter, page: 1 })),
|
setFilter: (filter) => setQuery((q) => ({ ...q, filter, page: 1 })),
|
||||||
|
|||||||
@ -1197,6 +1197,17 @@ button {
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pager__select {
|
||||||
|
margin-left: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.cell-clamp {
|
.cell-clamp {
|
||||||
max-width: 220px;
|
max-width: 220px;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user