Compare commits
2 Commits
main
...
fix/employ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1f841ac72 | ||
|
|
4612155f6f |
@ -144,8 +144,6 @@ export const ASSET_FIELDS = [
|
||||
},
|
||||
{ id: 'field_59', label: 'Asset Brand', 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. */
|
||||
@ -192,7 +190,8 @@ export const fetchAssetActivityFieldDefs = (activityId) =>
|
||||
|
||||
/**
|
||||
* "Asset Owner": a lookup into the employee table, like ASSIGNED's "Employee
|
||||
* data". Sent as { id, first_name } — see employeeLookup.
|
||||
* data". The API sample shows only { id }; it is sent as { id, first_name }
|
||||
* because the runtime checks lookups against their storage columns.
|
||||
*/
|
||||
export const ASSET_OWNER_FIELD = 'field_51'
|
||||
|
||||
@ -265,9 +264,11 @@ export const ASSET_ASSIGN_FIELDS = [
|
||||
]
|
||||
|
||||
/**
|
||||
* "Employee data": a lookup into the employee table, sent as { id, first_name }
|
||||
* like every employee lookup (see employeeLookup). It is picked from a list
|
||||
* of active employees, so it is sent alongside the form data.
|
||||
* "Employee data": a lookup into the employee table. The runtime checks it
|
||||
* against the lookup's storage columns (a Retired attempt failed with
|
||||
* "missing required storage column 'first_name'"), so it carries
|
||||
* { 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'
|
||||
|
||||
@ -438,17 +439,8 @@ export const ASSET_LOST_FIELDS = [
|
||||
/** "Last owner": a lookup into the employee table, like "Employee data". */
|
||||
export const LOST_EMPLOYEE_FIELD = 'field_35'
|
||||
|
||||
/**
|
||||
* 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,
|
||||
})
|
||||
/** An employee lookup value, in the shape the runtime validates. */
|
||||
const employeeLookup = (employee) => ({ id: employee.id, first_name: employee.first_name })
|
||||
|
||||
/**
|
||||
* Run a later activity (ASSIGNED, RETURNED, …) on an existing asset instance.
|
||||
@ -592,23 +584,21 @@ const EDITED_FIELDS = [
|
||||
* with the asset's instance_id.
|
||||
*
|
||||
* POST {APP_API_BASE_URL}/app/{APP_ID}/activity
|
||||
* { activity_id, workflow_uuid, instance_id, data: { field_80 … field_83 } }
|
||||
* { 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. (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.)
|
||||
* 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_80', label: 'Repair date', type: 'date' },
|
||||
{ id: 'field_81', label: 'Issue details', type: 'longtext' },
|
||||
{ id: 'field_82', label: 'Repair vendor', type: 'text' },
|
||||
{ id: 'field_83', label: 'Estimated cost', type: 'number' },
|
||||
{ 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'
|
||||
@ -1219,7 +1209,7 @@ export const ASSET_RICH_COLUMNS = [
|
||||
title: ['Warranty Status'],
|
||||
subtitle: [
|
||||
{ label: 'Warranty Start date', prefix: 'From' },
|
||||
'Warranty End date',
|
||||
{ label: 'Warranty End date', prefix: 'to' },
|
||||
],
|
||||
},
|
||||
]
|
||||
@ -1680,13 +1670,13 @@ export function assetEvents(config, row) {
|
||||
)
|
||||
}
|
||||
|
||||
if (present(val('field_80'))) {
|
||||
if (present(val('field_73'))) {
|
||||
add(
|
||||
HISTORY_ACTION.REPAIR,
|
||||
val('field_80'),
|
||||
val('field_73'),
|
||||
'',
|
||||
joinParts(val('field_82'), present(val('field_83')) && `Estimate ${val('field_83')}`),
|
||||
val('field_81'),
|
||||
joinParts(val('field_75'), present(val('field_76')) && `Estimate ${val('field_76')}`),
|
||||
val('field_74'),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -357,10 +357,10 @@ export const EMPLOYEE_RICH_COLUMNS = [
|
||||
{ key: 'contact', label: 'Contact', title: ['email'], subtitle: ['phone'] },
|
||||
{ key: 'role', label: 'Role', title: ['designation'], subtitle: ['department'] },
|
||||
{
|
||||
key: 'date',
|
||||
label: 'Date',
|
||||
title: [{ label: 'joining_date', prefix: 'Joined' }],
|
||||
subtitle: ['Employee type'],
|
||||
key: 'type',
|
||||
label: 'Type',
|
||||
title: ['Employee type'],
|
||||
subtitle: [{ label: 'joining_date', prefix: 'Joined' }],
|
||||
},
|
||||
]
|
||||
|
||||
@ -390,11 +390,7 @@ export async function fetchEmployeeListPage(templateUid, query = {}) {
|
||||
.map((row) => ({ ...row, employment: isEndState(row) ? 'Inactive' : 'Active' }))
|
||||
.filter((row) => filter === 'all' || row.employment.toLowerCase() === filter)
|
||||
|
||||
// The view's own "status" column isn't wanted in the list — the derived
|
||||
// Employment badge below already shows each row's Active / Inactive state.
|
||||
const fields = (config?.fields ?? []).filter(
|
||||
(c) => ![c.field_key, c.output_label].some((l) => normLabel(l) === 'status'),
|
||||
)
|
||||
const fields = config?.fields ?? []
|
||||
const totalPages = Math.max(1, Math.ceil(data.length / limit))
|
||||
return {
|
||||
config: { ...config, fields: [...fields, EMPLOYMENT_COLUMN] },
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { APP_API_BASE_URL, ApiError, apiFetch, getStoredToken } from './client'
|
||||
import { APP_API_BASE_URL, apiFetch } from './client'
|
||||
|
||||
/**
|
||||
* Record view data source.
|
||||
@ -104,137 +104,6 @@ export async function fetchAllRecordRows(templateUid, query = {}) {
|
||||
return { config, rows }
|
||||
}
|
||||
|
||||
/**
|
||||
* Columns the Zino "Asset" screen exports from the asset table view
|
||||
* (RECORD_VIEWS.assetTable). Names are the backend's own, typos included
|
||||
* (warrenty_end_date, warrenty_status).
|
||||
*/
|
||||
export const ASSET_EXPORT_COLUMNS = [
|
||||
'monthly_allowance',
|
||||
'asset_classification',
|
||||
'warranty_start_date',
|
||||
'asset_identifier',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'asset_details',
|
||||
'model',
|
||||
'serial_number',
|
||||
'purchase_date',
|
||||
'purchase_cost',
|
||||
'asset_state',
|
||||
'warrenty_end_date',
|
||||
'hard_drive',
|
||||
'asset_owner',
|
||||
'image',
|
||||
'warrenty_status',
|
||||
'asset_brand',
|
||||
'asset_location',
|
||||
'cpu',
|
||||
]
|
||||
|
||||
/** Columns the Zino "Employee" screen exports from RECORD_VIEWS.employees. */
|
||||
export const EMPLOYEE_EXPORT_COLUMNS = [
|
||||
'id',
|
||||
'employee_code',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
'phone',
|
||||
'department',
|
||||
'designation',
|
||||
'joining_date',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
]
|
||||
|
||||
/**
|
||||
* Download every row of a record view as a file, the way the Zino screen's
|
||||
* export button does:
|
||||
*
|
||||
* POST {RUNTIME}/app/{APP_ID}/view/recordview/export
|
||||
* { rv_template_uid, columns: [{ name, value_path }], preset_alias, params, search, filters }
|
||||
*
|
||||
* The file is saved as `fileName` unless the server names it
|
||||
* (Content-Disposition). A JSON reply carrying a link is followed instead; one
|
||||
* without a link means the file will be emailed.
|
||||
* @returns {Promise<{emailed: boolean, data?: object}>}
|
||||
*/
|
||||
export async function downloadRecordView(
|
||||
templateUid,
|
||||
columns,
|
||||
{ fileName = 'export.xlsx', search = '', filters = [], params = {}, presetAlias = '' } = {},
|
||||
) {
|
||||
const token = getStoredToken()
|
||||
let response
|
||||
try {
|
||||
response = await fetch(`${APP_API_BASE_URL}/app/${RECORD_VIEW_APP_ID}/view/recordview/export`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
accept: '*/*',
|
||||
'content-type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` }),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
rv_template_uid: templateUid,
|
||||
columns: columns.map((name) => ({ name, value_path: name })),
|
||||
preset_alias: presetAlias,
|
||||
params,
|
||||
search,
|
||||
filters,
|
||||
}),
|
||||
})
|
||||
} catch (err) {
|
||||
throw new ApiError('Unable to reach the server. Check your connection and try again.', {
|
||||
status: 0,
|
||||
data: err,
|
||||
})
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => '')
|
||||
throw new ApiError(text.trim() || `Export failed (${response.status})`, {
|
||||
status: response.status,
|
||||
data: text,
|
||||
})
|
||||
}
|
||||
|
||||
const type = response.headers.get('content-type') || ''
|
||||
if (type.includes('application/json')) {
|
||||
const data = await response.json()
|
||||
const url = data?.url || data?.download_url || data?.data?.url || data?.data?.download_url
|
||||
// No link: the export runs in the background and Zino emails the file to
|
||||
// the signed-in user (the owner of the token).
|
||||
if (!url) return { emailed: true, data }
|
||||
saveFile(url, data?.file_name || fileName)
|
||||
return { emailed: false }
|
||||
}
|
||||
|
||||
const blob = await response.blob()
|
||||
const url = URL.createObjectURL(blob)
|
||||
saveFile(url, fileNameFrom(response.headers.get('content-disposition')) || fileName)
|
||||
// Give the browser a moment to start the download before freeing the blob.
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
||||
return { emailed: false }
|
||||
}
|
||||
|
||||
/** 'attachment; filename="assets.xlsx"' -> 'assets.xlsx'. */
|
||||
function fileNameFrom(disposition) {
|
||||
if (!disposition) return ''
|
||||
const utf8 = /filename\*=UTF-8''([^;]+)/i.exec(disposition)
|
||||
if (utf8) return decodeURIComponent(utf8[1])
|
||||
const plain = /filename="?([^";]+)"?/i.exec(disposition)
|
||||
return plain ? plain[1].trim() : ''
|
||||
}
|
||||
|
||||
function saveFile(url, fileName) {
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
}
|
||||
|
||||
/** Field types the backend reports that should render as a date. */
|
||||
const DATE_TYPES = new Set(['date', 'timestamp without time zone', 'timestamp with time zone'])
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import {
|
||||
AssetStateBadge,
|
||||
Avatar,
|
||||
Button,
|
||||
EmployeeStatusBadge,
|
||||
EmptyState,
|
||||
HistoryActionBadge,
|
||||
@ -16,16 +17,11 @@ import { FileLinks, imageSources } from './FieldValue'
|
||||
/**
|
||||
* Renders a record view straight from the backend response: the columns are
|
||||
* whatever `config.fields` returns, in that order, with the backend's own
|
||||
* 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.
|
||||
* labels. Paging, sorting and search are handled by the server.
|
||||
* `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)
|
||||
* 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({
|
||||
fields,
|
||||
rows,
|
||||
@ -33,7 +29,7 @@ export default function RecordView({
|
||||
loading,
|
||||
error,
|
||||
query,
|
||||
setLimit,
|
||||
setPage,
|
||||
setSearch,
|
||||
setSort,
|
||||
rowKey = 'instance_id',
|
||||
@ -50,8 +46,6 @@ export default function RecordView({
|
||||
return () => clearTimeout(id)
|
||||
}, [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.
|
||||
const searchable = fields
|
||||
.flatMap((f) => (f.data_type === 'rich' ? [...f.title, ...f.subtitle] : [f]))
|
||||
@ -65,20 +59,20 @@ export default function RecordView({
|
||||
setSort(sortKey(field), dir)
|
||||
}
|
||||
|
||||
const page = pagination?.page ?? 1
|
||||
const totalPages = pagination?.total_pages ?? 1
|
||||
const total = pagination?.total_count ?? rows.length
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toolbar count={total} countLabel="records">
|
||||
<SearchInput
|
||||
value={term}
|
||||
onChange={setTerm}
|
||||
placeholder={
|
||||
searchable.length > 0
|
||||
? `Search by ${searchable.map((f) => displayLabel(f.output_label)).join(', ')}…`
|
||||
: 'Search…'
|
||||
}
|
||||
/>
|
||||
{searchable.length > 0 && (
|
||||
<SearchInput
|
||||
value={term}
|
||||
onChange={setTerm}
|
||||
placeholder={`Search by ${searchable.map((f) => displayLabel(f.output_label)).join(', ')}…`}
|
||||
/>
|
||||
)}
|
||||
{toolbar}
|
||||
{loading && <span className="toolbar__count">Loading…</span>}
|
||||
</Toolbar>
|
||||
@ -145,26 +139,26 @@ export default function RecordView({
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{total > 0 && (
|
||||
{totalPages > 1 && (
|
||||
<div className="pager">
|
||||
<label className="pager__label">
|
||||
Rows{' '}
|
||||
<select
|
||||
className="pager__select"
|
||||
value={query.limit}
|
||||
disabled={loading}
|
||||
onChange={(e) => setLimit(Number(e.target.value))}
|
||||
>
|
||||
{ROW_OPTIONS.map((n) => (
|
||||
<option key={n} value={n}>
|
||||
{n}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<Button
|
||||
size="sm"
|
||||
icon="chevronLeft"
|
||||
disabled={page <= 1 || loading}
|
||||
onClick={() => setPage(page - 1)}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<span className="pager__label">
|
||||
Showing {Math.min(rows.length, total)} of {total} records
|
||||
Page {page} of {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={page >= totalPages || loading}
|
||||
onClick={() => setPage(page + 1)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -24,7 +24,6 @@ const PATHS = {
|
||||
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',
|
||||
logout: 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9',
|
||||
download: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3',
|
||||
}
|
||||
|
||||
export default function Icon({ name, size = 16, strokeWidth = 1.8, ...rest }) {
|
||||
|
||||
@ -71,7 +71,6 @@ export default function useRecordView(
|
||||
error,
|
||||
query,
|
||||
setPage: (page) => setQuery((q) => ({ ...q, page })),
|
||||
setLimit: (limit) => setQuery((q) => ({ ...q, limit, page: 1 })),
|
||||
setSearch: (search) => setQuery((q) => ({ ...q, search, page: 1 })),
|
||||
setSort: (sortBy, sortDir) => setQuery((q) => ({ ...q, sortBy, sortDir, page: 1 })),
|
||||
setFilter: (filter) => setQuery((q) => ({ ...q, filter, page: 1 })),
|
||||
|
||||
@ -12,18 +12,10 @@ import {
|
||||
import { emptyValues, validateActivity } from '../../api/activityForm'
|
||||
import { useAuth } from '../../store/AuthContext'
|
||||
|
||||
// Asset Brand and Asset Location come first, as on the Edit form; the rest
|
||||
// keep the builder's order.
|
||||
const FIRST_FIELDS = ['field_59', 'field_61']
|
||||
const FORM_FIELDS = [
|
||||
...FIRST_FIELDS.map((id) => ASSET_FIELDS.find((f) => f.id === id)),
|
||||
...ASSET_FIELDS.filter((f) => !FIRST_FIELDS.includes(f.id)),
|
||||
]
|
||||
|
||||
// Asset Owner sits after Condition, as in the builder.
|
||||
const OWNER_AFTER = FORM_FIELDS.findIndex((f) => f.id === 'field_47') + 1
|
||||
const FIELDS_BEFORE_OWNER = FORM_FIELDS.slice(0, OWNER_AFTER)
|
||||
const FIELDS_AFTER_OWNER = FORM_FIELDS.slice(OWNER_AFTER)
|
||||
const OWNER_AFTER = ASSET_FIELDS.findIndex((f) => f.id === 'field_47') + 1
|
||||
const FIELDS_BEFORE_OWNER = ASSET_FIELDS.slice(0, OWNER_AFTER)
|
||||
const FIELDS_AFTER_OWNER = ASSET_FIELDS.slice(OWNER_AFTER)
|
||||
|
||||
const upload = (fieldId, file) => uploadAssetFile(ASSET_INIT_ACTIVITY_ID, fieldId, file)
|
||||
|
||||
|
||||
@ -3,10 +3,8 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { Button, Card, Icon, PageHeader } from '../../components/ui'
|
||||
import RecordView from '../../components/activity/RecordView'
|
||||
import useRecordView from '../../hooks/useRecordView'
|
||||
import { ASSET_EXPORT_COLUMNS, RECORD_VIEWS, downloadRecordView } from '../../api/recordView'
|
||||
import { RECORD_VIEWS } from '../../api/recordView'
|
||||
import { assetRowId, withAssetListDisplay } from '../../api/assets'
|
||||
import { useAuth } from '../../store/AuthContext'
|
||||
import { todayISO } from '../../utils/format'
|
||||
import AddAssetModal from './AddAssetModal'
|
||||
|
||||
const VIEW = RECORD_VIEWS.assets
|
||||
@ -22,38 +20,9 @@ export default function AssetList() {
|
||||
sortBy: 'idgen_2',
|
||||
sortDir: 'desc',
|
||||
})
|
||||
const { logout, user } = useAuth()
|
||||
const [adding, setAdding] = useState(false)
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const [notice, setNotice] = useState(null)
|
||||
|
||||
/** Downloads every asset (all pages) from the asset table view. */
|
||||
async function handleExport() {
|
||||
setExporting(true)
|
||||
try {
|
||||
const { emailed } = await downloadRecordView(
|
||||
RECORD_VIEWS.assetTable.templateUid,
|
||||
ASSET_EXPORT_COLUMNS,
|
||||
{ fileName: `assets-${todayISO()}.xlsx` },
|
||||
)
|
||||
if (emailed) {
|
||||
setNotice({
|
||||
tone: 'success',
|
||||
text: `Export started. The file will be emailed to ${user?.email || 'you'}.`,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
if (err?.status === 401) {
|
||||
logout()
|
||||
navigate('/login', { replace: true })
|
||||
return
|
||||
}
|
||||
setNotice({ tone: 'danger', text: err?.message || 'Could not download the assets.' })
|
||||
} finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
/** `values` is keyed by Zino field id — see ASSET_FIELDS in api/assets.js. */
|
||||
function handleCreated(values) {
|
||||
setNotice({
|
||||
@ -70,14 +39,9 @@ export default function AssetList() {
|
||||
title="Assets"
|
||||
subtitle="Every device in the inventory, read live from the asset workflow."
|
||||
actions={
|
||||
<>
|
||||
<Button icon="download" onClick={handleExport} disabled={exporting}>
|
||||
{exporting ? 'Downloading…' : 'Export'}
|
||||
</Button>
|
||||
<Button variant="primary" icon="plus" onClick={() => setAdding(true)}>
|
||||
Add Asset
|
||||
</Button>
|
||||
</>
|
||||
<Button variant="primary" icon="plus" onClick={() => setAdding(true)}>
|
||||
Add Asset
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { Button, Card, Icon, PageHeader, SelectInput } from '../../components/ui'
|
||||
import RecordView from '../../components/activity/RecordView'
|
||||
import useRecordView from '../../hooks/useRecordView'
|
||||
import { EMPLOYEE_EXPORT_COLUMNS, RECORD_VIEWS, downloadRecordView } from '../../api/recordView'
|
||||
import { RECORD_VIEWS } from '../../api/recordView'
|
||||
import {
|
||||
EMPLOYEE_RICH_COLUMNS,
|
||||
EMPLOYMENT_FILTERS,
|
||||
@ -12,14 +12,13 @@ import {
|
||||
} from '../../api/employees'
|
||||
import { withRichColumns } from '../../api/richColumns'
|
||||
import { useAuth } from '../../store/AuthContext'
|
||||
import { todayISO } from '../../utils/format'
|
||||
import AddEmployeeModal from './AddEmployeeModal'
|
||||
|
||||
const VIEW = RECORD_VIEWS.employees
|
||||
|
||||
export default function EmployeeList() {
|
||||
const navigate = useNavigate()
|
||||
const { logout, user } = useAuth()
|
||||
const { logout } = useAuth()
|
||||
// The table misses Update fields Zino doesn't map and knows nothing of the
|
||||
// workflow state, so this fetcher overlays both and filters Active/Inactive.
|
||||
// Related columns (name and code, email and phone…) are grouped into one.
|
||||
@ -30,34 +29,8 @@ export default function EmployeeList() {
|
||||
})
|
||||
const [adding, setAdding] = useState(false)
|
||||
const [opening, setOpening] = useState(false)
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const [notice, setNotice] = useState(null)
|
||||
|
||||
/** Exports every employee (all pages) from the employee view. */
|
||||
async function handleExport() {
|
||||
setExporting(true)
|
||||
try {
|
||||
const { emailed } = await downloadRecordView(VIEW.templateUid, EMPLOYEE_EXPORT_COLUMNS, {
|
||||
fileName: `employees-${todayISO()}.xlsx`,
|
||||
})
|
||||
if (emailed) {
|
||||
setNotice({
|
||||
tone: 'success',
|
||||
text: `Export started. The file will be emailed to ${user?.email || 'you'}.`,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
if (err?.status === 401) {
|
||||
logout()
|
||||
navigate('/login', { replace: true })
|
||||
return
|
||||
}
|
||||
setNotice({ tone: 'danger', text: err?.message || 'Could not download the employees.' })
|
||||
} finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
/** `values` is keyed by Zino field id — see EMPLOYEE_FIELDS in api/employees.js. */
|
||||
function handleCreated(values) {
|
||||
const name = [values.field_1, values.field_34].filter(Boolean).join(' ').trim()
|
||||
@ -105,14 +78,9 @@ export default function EmployeeList() {
|
||||
title="Employees"
|
||||
subtitle="All staff on record, read live from the employee workflow."
|
||||
actions={
|
||||
<>
|
||||
<Button icon="download" onClick={handleExport} disabled={exporting}>
|
||||
{exporting ? 'Exporting…' : 'Export'}
|
||||
</Button>
|
||||
<Button variant="primary" icon="plus" onClick={() => setAdding(true)}>
|
||||
Add Employee
|
||||
</Button>
|
||||
</>
|
||||
<Button variant="primary" icon="plus" onClick={() => setAdding(true)}>
|
||||
Add Employee
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@ -1197,17 +1197,6 @@ button {
|
||||
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 {
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user