feat: add queue summary widget and layout fixes
This commit is contained in:
parent
f7976e2c39
commit
a542846313
12 changed files with 350 additions and 45 deletions
|
|
@ -132,6 +132,29 @@ function areColumnConfigsEqual(a: DeviceInventoryColumnConfig[], b: DeviceInvent
|
|||
return a.every((col, idx) => col.key === b[idx]?.key && (col.label ?? "") === (b[idx]?.label ?? ""))
|
||||
}
|
||||
|
||||
function formatDeviceCustomFieldDisplay(entry?: { value?: unknown; displayValue?: string }): string {
|
||||
if (!entry) return "—"
|
||||
if (typeof entry.displayValue === "string" && entry.displayValue.trim().length > 0) {
|
||||
return entry.displayValue
|
||||
}
|
||||
const raw = entry.value
|
||||
if (raw === null || raw === undefined) return "—"
|
||||
if (Array.isArray(raw)) {
|
||||
const values = raw
|
||||
.map((item) => (item === null || item === undefined ? "" : String(item).trim()))
|
||||
.filter((item) => item.length > 0)
|
||||
return values.length > 0 ? values.join(", ") : "—"
|
||||
}
|
||||
if (typeof raw === "boolean") {
|
||||
return raw ? "Sim" : "Não"
|
||||
}
|
||||
if (typeof raw === "number") {
|
||||
return Number.isFinite(raw) ? String(raw) : "—"
|
||||
}
|
||||
const asString = String(raw).trim()
|
||||
return asString.length > 0 ? asString : "—"
|
||||
}
|
||||
|
||||
type DeviceAlertEntry = {
|
||||
id: string
|
||||
kind: string
|
||||
|
|
@ -886,7 +909,7 @@ export type DevicesQueryItem = {
|
|||
lastPostureAt?: number | null
|
||||
linkedUsers?: Array<{ id: string; email: string; name: string }>
|
||||
remoteAccessEntries: DeviceRemoteAccessEntry[]
|
||||
customFields?: Array<{ fieldKey: string; label: string; type?: string; value: unknown; displayValue?: string }>
|
||||
customFields?: Array<{ fieldId?: string; fieldKey: string; label: string; type?: string; value: unknown; displayValue?: string }>
|
||||
}
|
||||
|
||||
export function normalizeDeviceItem(raw: Record<string, unknown>): DevicesQueryItem {
|
||||
|
|
@ -3421,6 +3444,49 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
[deviceFieldDefs]
|
||||
)
|
||||
|
||||
const displayCustomFields = useMemo(() => {
|
||||
const definitions = deviceFieldDefs ?? []
|
||||
const values = device?.customFields ?? []
|
||||
const result: Array<{ key: string; label: string; value: string }> = []
|
||||
const valueMap = new Map<string, (typeof values)[number]>()
|
||||
|
||||
values.forEach((field) => {
|
||||
if (field.fieldId) {
|
||||
valueMap.set(String(field.fieldId), field)
|
||||
}
|
||||
if (field.fieldKey) {
|
||||
valueMap.set(field.fieldKey, field)
|
||||
}
|
||||
})
|
||||
|
||||
const used = new Set<string>()
|
||||
definitions.forEach((definition) => {
|
||||
const idKey = String(definition.id)
|
||||
const valueEntry = valueMap.get(idKey) ?? valueMap.get(definition.key)
|
||||
used.add(idKey)
|
||||
result.push({
|
||||
key: idKey,
|
||||
label: definition.label,
|
||||
value: formatDeviceCustomFieldDisplay(valueEntry),
|
||||
})
|
||||
})
|
||||
|
||||
values.forEach((field) => {
|
||||
const idKey = field.fieldId ? String(field.fieldId) : undefined
|
||||
const keyKey = field.fieldKey ?? field.label
|
||||
const compositeKey = idKey ?? keyKey
|
||||
if (!compositeKey || used.has(compositeKey)) return
|
||||
used.add(compositeKey)
|
||||
result.push({
|
||||
key: compositeKey,
|
||||
label: field.label,
|
||||
value: formatDeviceCustomFieldDisplay(field),
|
||||
})
|
||||
})
|
||||
|
||||
return result
|
||||
}, [deviceFieldDefs, device?.customFields])
|
||||
|
||||
const handleSaveCustomFields = useCallback(async () => {
|
||||
if (!device || !convexUserId) return
|
||||
try {
|
||||
|
|
@ -3550,14 +3616,14 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
{/* Campos personalizados (posicionado logo após métricas) */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-semibold text-neutral-900">Campos personalizados</h4>
|
||||
<Badge variant="outline" className="rounded-full px-2.5 py-0.5 text-[11px] font-semibold">
|
||||
{(device.customFields ?? []).length}
|
||||
{displayCustomFields.length}
|
||||
</Badge>
|
||||
</div>
|
||||
{(!device.customFields || device.customFields.length === 0) ? (
|
||||
{displayCustomFields.length === 0 ? (
|
||||
<p className="text-xs text-neutral-500">Nenhum campo personalizado definido para este dispositivo.</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
@ -3575,12 +3641,12 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{device.customFields && device.customFields.length > 0 ? (
|
||||
{displayCustomFields.length > 0 ? (
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{(device.customFields ?? []).map((f, idx) => (
|
||||
<div key={`${f.fieldKey}-${idx}`} className="rounded-lg border border-slate-200 bg-white p-3 text-sm">
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-500">{f.label}</p>
|
||||
<p className="mt-1 text-neutral-800">{(f.displayValue ?? f.value ?? "—") as string}</p>
|
||||
{displayCustomFields.map((field) => (
|
||||
<div key={field.key} className="rounded-lg border border-slate-200 bg-white p-3 text-sm">
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-500">{field.label}</p>
|
||||
<p className="mt-1 text-neutral-800">{field.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue