Compare commits
No commits in common. "5dead10c22df6a374228fe7d98b747c1b0dc27e0" and "c4ccdbf7d379a271a58821937484be6bf1a7ba99" have entirely different histories.
5dead10c22
...
c4ccdbf7d3
@ -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.
|
* Record view data source.
|
||||||
@ -104,138 +104,6 @@ export async function fetchAllRecordRows(templateUid, query = {}) {
|
|||||||
return { config, rows }
|
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',
|
|
||||||
'status',
|
|
||||||
'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. */
|
/** 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'])
|
const DATE_TYPES = new Set(['date', 'timestamp without time zone', 'timestamp with time zone'])
|
||||||
|
|
||||||
|
|||||||
@ -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',
|
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',
|
||||||
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 }) {
|
export default function Icon({ name, size = 16, strokeWidth = 1.8, ...rest }) {
|
||||||
|
|||||||
@ -12,18 +12,10 @@ import {
|
|||||||
import { emptyValues, validateActivity } from '../../api/activityForm'
|
import { emptyValues, validateActivity } from '../../api/activityForm'
|
||||||
import { useAuth } from '../../store/AuthContext'
|
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.
|
// Asset Owner sits after Condition, as in the builder.
|
||||||
const OWNER_AFTER = FORM_FIELDS.findIndex((f) => f.id === 'field_47') + 1
|
const OWNER_AFTER = ASSET_FIELDS.findIndex((f) => f.id === 'field_47') + 1
|
||||||
const FIELDS_BEFORE_OWNER = FORM_FIELDS.slice(0, OWNER_AFTER)
|
const FIELDS_BEFORE_OWNER = ASSET_FIELDS.slice(0, OWNER_AFTER)
|
||||||
const FIELDS_AFTER_OWNER = FORM_FIELDS.slice(OWNER_AFTER)
|
const FIELDS_AFTER_OWNER = ASSET_FIELDS.slice(OWNER_AFTER)
|
||||||
|
|
||||||
const upload = (fieldId, file) => uploadAssetFile(ASSET_INIT_ACTIVITY_ID, fieldId, file)
|
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 { Button, Card, Icon, PageHeader } from '../../components/ui'
|
||||||
import RecordView from '../../components/activity/RecordView'
|
import RecordView from '../../components/activity/RecordView'
|
||||||
import useRecordView from '../../hooks/useRecordView'
|
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 { assetRowId, withAssetListDisplay } from '../../api/assets'
|
||||||
import { useAuth } from '../../store/AuthContext'
|
|
||||||
import { todayISO } from '../../utils/format'
|
|
||||||
import AddAssetModal from './AddAssetModal'
|
import AddAssetModal from './AddAssetModal'
|
||||||
|
|
||||||
const VIEW = RECORD_VIEWS.assets
|
const VIEW = RECORD_VIEWS.assets
|
||||||
@ -22,38 +20,9 @@ export default function AssetList() {
|
|||||||
sortBy: 'idgen_2',
|
sortBy: 'idgen_2',
|
||||||
sortDir: 'desc',
|
sortDir: 'desc',
|
||||||
})
|
})
|
||||||
const { logout, user } = useAuth()
|
|
||||||
const [adding, setAdding] = useState(false)
|
const [adding, setAdding] = useState(false)
|
||||||
const [exporting, setExporting] = useState(false)
|
|
||||||
const [notice, setNotice] = useState(null)
|
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. */
|
/** `values` is keyed by Zino field id — see ASSET_FIELDS in api/assets.js. */
|
||||||
function handleCreated(values) {
|
function handleCreated(values) {
|
||||||
setNotice({
|
setNotice({
|
||||||
@ -70,14 +39,9 @@ export default function AssetList() {
|
|||||||
title="Assets"
|
title="Assets"
|
||||||
subtitle="Every device in the inventory, read live from the asset workflow."
|
subtitle="Every device in the inventory, read live from the asset workflow."
|
||||||
actions={
|
actions={
|
||||||
<>
|
<Button variant="primary" icon="plus" onClick={() => setAdding(true)}>
|
||||||
<Button icon="download" onClick={handleExport} disabled={exporting}>
|
Add Asset
|
||||||
{exporting ? 'Downloading…' : 'Export'}
|
</Button>
|
||||||
</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 { Button, Card, Icon, PageHeader, SelectInput } from '../../components/ui'
|
||||||
import RecordView from '../../components/activity/RecordView'
|
import RecordView from '../../components/activity/RecordView'
|
||||||
import useRecordView from '../../hooks/useRecordView'
|
import useRecordView from '../../hooks/useRecordView'
|
||||||
import { EMPLOYEE_EXPORT_COLUMNS, RECORD_VIEWS, downloadRecordView } from '../../api/recordView'
|
import { RECORD_VIEWS } from '../../api/recordView'
|
||||||
import {
|
import {
|
||||||
EMPLOYEE_RICH_COLUMNS,
|
EMPLOYEE_RICH_COLUMNS,
|
||||||
EMPLOYMENT_FILTERS,
|
EMPLOYMENT_FILTERS,
|
||||||
@ -12,14 +12,13 @@ import {
|
|||||||
} from '../../api/employees'
|
} from '../../api/employees'
|
||||||
import { withRichColumns } from '../../api/richColumns'
|
import { withRichColumns } from '../../api/richColumns'
|
||||||
import { useAuth } from '../../store/AuthContext'
|
import { useAuth } from '../../store/AuthContext'
|
||||||
import { todayISO } from '../../utils/format'
|
|
||||||
import AddEmployeeModal from './AddEmployeeModal'
|
import AddEmployeeModal from './AddEmployeeModal'
|
||||||
|
|
||||||
const VIEW = RECORD_VIEWS.employees
|
const VIEW = RECORD_VIEWS.employees
|
||||||
|
|
||||||
export default function EmployeeList() {
|
export default function EmployeeList() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { logout, user } = useAuth()
|
const { logout } = useAuth()
|
||||||
// The table misses Update fields Zino doesn't map and knows nothing of the
|
// 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.
|
// workflow state, so this fetcher overlays both and filters Active/Inactive.
|
||||||
// Related columns (name and code, email and phone…) are grouped into one.
|
// 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 [adding, setAdding] = useState(false)
|
||||||
const [opening, setOpening] = useState(false)
|
const [opening, setOpening] = useState(false)
|
||||||
const [exporting, setExporting] = useState(false)
|
|
||||||
const [notice, setNotice] = useState(null)
|
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. */
|
/** `values` is keyed by Zino field id — see EMPLOYEE_FIELDS in api/employees.js. */
|
||||||
function handleCreated(values) {
|
function handleCreated(values) {
|
||||||
const name = [values.field_1, values.field_34].filter(Boolean).join(' ').trim()
|
const name = [values.field_1, values.field_34].filter(Boolean).join(' ').trim()
|
||||||
@ -105,14 +78,9 @@ export default function EmployeeList() {
|
|||||||
title="Employees"
|
title="Employees"
|
||||||
subtitle="All staff on record, read live from the employee workflow."
|
subtitle="All staff on record, read live from the employee workflow."
|
||||||
actions={
|
actions={
|
||||||
<>
|
<Button variant="primary" icon="plus" onClick={() => setAdding(true)}>
|
||||||
<Button icon="download" onClick={handleExport} disabled={exporting}>
|
Add Employee
|
||||||
{exporting ? 'Exporting…' : 'Export'}
|
</Button>
|
||||||
</Button>
|
|
||||||
<Button variant="primary" icon="plus" onClick={() => setAdding(true)}>
|
|
||||||
Add Employee
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user