"use client"; import { FormEvent, useMemo, useState } from "react"; import { AlertTriangle, CheckCircle2, Search, Wrench, XCircle } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog } from "@/components/ui/dialog"; import { Input, Textarea } from "@/components/ui/input"; import { Select } from "@/components/ui/select"; import { Table } from "@/components/ui/table"; import type { ApiResult } from "@/modules/core/server/api"; import type { DeviceAsset, DeviceAssetInput, DeviceAssetStatus, DeviceOperationsData, MaintenanceTicket, MaintenanceTicketInput, MaintenanceTicketPriority, MaintenanceTicketStatus } from "@/modules/devices/types"; import { DEVICE_ASSET_STATUS_LABELS, DEVICE_ASSET_STATUS_VALUES, MAINTENANCE_TICKET_PRIORITY_LABELS, MAINTENANCE_TICKET_PRIORITY_VALUES, MAINTENANCE_TICKET_STATUS_LABELS, MAINTENANCE_TICKET_STATUS_VALUES } from "@/modules/devices/types"; type DevicesWorkspaceClientProps = { canManage: boolean; initialData: DeviceOperationsData; }; type DialogState = | { kind: "device"; mode: "create"; value: DeviceAssetInput } | { kind: "device"; mode: "edit"; id: string; value: DeviceAssetInput } | { kind: "ticket"; mode: "create"; value: MaintenanceTicketInput } | { kind: "ticket"; mode: "edit"; id: string; value: MaintenanceTicketInput }; const emptyDevice: DeviceAssetInput = { name: "", code: "", category: "", location: "", status: "active", lastInspectedAt: undefined, notes: "", }; const emptyTicket: MaintenanceTicketInput = { deviceId: undefined, title: "", description: "", priority: "normal", status: "open", assigneeLabel: "", resolutionNotes: "", }; function formatDateTime(value: string | undefined): string { return value ? new Date(value).toLocaleString("zh-CN") : "-"; } function deviceToInput(device: DeviceAsset): DeviceAssetInput { return { name: device.name, code: device.code, category: device.category, location: device.location, status: device.status, lastInspectedAt: device.lastInspectedAt, notes: device.notes, }; } function ticketToInput(ticket: MaintenanceTicket): MaintenanceTicketInput { return { deviceId: ticket.deviceId, title: ticket.title, description: ticket.description, priority: ticket.priority, status: ticket.status, assigneeLabel: ticket.assigneeLabel, resolutionNotes: ticket.resolutionNotes, }; } function badgeVariant(status: string): "danger" | "secondary" | "success" | "warning" { if (status === "active" || status === "resolved" || status === "closed") { return "success"; } if (status === "maintenance" || status === "open" || status === "assigned" || status === "urgent") { return "warning"; } if (status === "disabled" || status === "retired" || status === "cancelled") { return "danger"; } return "secondary"; } export function DevicesWorkspaceClient({ canManage, initialData }: DevicesWorkspaceClientProps): React.ReactElement { const [data, setData] = useState(initialData); const [query, setQuery] = useState(""); const [deviceStatus, setDeviceStatus] = useState<"all" | DeviceAssetStatus>("all"); const [ticketStatus, setTicketStatus] = useState<"all" | MaintenanceTicketStatus>("all"); const [dialog, setDialog] = useState(); const [message, setMessage] = useState(""); const [isPending, setIsPending] = useState(false); const normalizedQuery = query.trim().toLowerCase(); const filteredDevices = useMemo( () => data.devices.filter((device) => { const matchesQuery = !normalizedQuery || [device.name, device.code, device.category, device.location, device.notes].join(" ").toLowerCase().includes(normalizedQuery); const matchesStatus = deviceStatus === "all" || device.status === deviceStatus; return matchesQuery && matchesStatus; }), [data.devices, deviceStatus, normalizedQuery], ); const filteredTickets = useMemo( () => data.tickets.filter((ticket) => { const matchesQuery = !normalizedQuery || [ticket.title, ticket.description, ticket.deviceName, ticket.assigneeLabel].join(" ").toLowerCase().includes(normalizedQuery); const matchesStatus = ticketStatus === "all" || ticket.status === ticketStatus; return matchesQuery && matchesStatus; }), [data.tickets, normalizedQuery, ticketStatus], ); async function refreshData(): Promise { const response = await fetch("/api/devices/assets"); const result = (await response.json()) as ApiResult<{ data: DeviceOperationsData }>; if (result.success) { setData(result.data); } } async function submitDialog(event: FormEvent): Promise { event.preventDefault(); if (!dialog || !canManage) { setMessage("当前角色无权维护设备运维"); return; } const isCreate = dialog.mode === "create"; const path = dialog.kind === "device" ? isCreate ? "/api/devices/assets" : `/api/devices/assets/${dialog.id}` : isCreate ? "/api/devices/tickets" : `/api/devices/tickets/${dialog.id}`; setIsPending(true); const response = await fetch(path, { method: isCreate ? "POST" : "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify(dialog.value), }); const result = (await response.json()) as ApiResult>; setIsPending(false); setMessage(result.reason); if (result.success) { setDialog(undefined); await refreshData(); } } async function remove(kind: "device" | "ticket", id: string): Promise { if (!canManage) { setMessage("当前角色无权删除设备运维记录"); return; } const path = kind === "device" ? `/api/devices/assets/${id}` : `/api/devices/tickets/${id}`; setIsPending(true); const response = await fetch(path, { method: "DELETE" }); const result = (await response.json()) as ApiResult>; setIsPending(false); setMessage(result.reason); if (result.success) { await refreshData(); } } return (

设备运维

维护设备台账、维修工单和运行状态。

setTicketStatus(value as "all" | MaintenanceTicketStatus)} options={[{ value: "all", label: "全部工单状态" }, ...MAINTENANCE_TICKET_STATUS_VALUES.map((value) => ({ value, label: MAINTENANCE_TICKET_STATUS_LABELS[value] }))]} value={ticketStatus} />
{message ?

{message}

: null} 设备台账 设备 分类 位置 状态 最近巡检 操作 {filteredDevices.map((device) => (

{device.name}

{device.code} / {device.notes || "-"}

{device.category || "-"} {device.location || "-"} {DEVICE_ASSET_STATUS_LABELS[device.status]} {formatDateTime(device.lastInspectedAt)}
))} {filteredDevices.length === 0 ? 暂无设备 : null}
维修工单 工单 设备 优先级 状态 负责人 更新时间 操作 {filteredTickets.map((ticket) => (

{ticket.title}

{ticket.description || ticket.resolutionNotes || "-"}

{ticket.deviceName || "-"} {MAINTENANCE_TICKET_PRIORITY_LABELS[ticket.priority]} {MAINTENANCE_TICKET_STATUS_LABELS[ticket.status]} {ticket.assigneeLabel || "-"} {formatDateTime(ticket.updatedAt)}
))} {filteredTickets.length === 0 ? 暂无维修工单 : null}
setDialog(undefined)} open={dialog !== undefined} title={dialog?.kind === "device" ? "设备台账" : "维修工单"} width="lg"> {dialog ? (
{dialog.kind === "device" ? : }
) : null}
); } function DeviceForm({ dialog, setDialog }: { dialog: Extract; setDialog: (dialog: DialogState) => void }): React.ReactElement { const value = dialog.value; function update(next: Partial): void { setDialog({ ...dialog, value: { ...value, ...next } }); } return ( <> update({ name: event.target.value })} required value={value.name} /> update({ code: event.target.value })} required value={value.code} /> update({ category: event.target.value })} value={value.category} /> update({ location: event.target.value })} value={value.location} /> update({ lastInspectedAt: event.target.value || undefined })} type="datetime-local" value={value.lastInspectedAt ? value.lastInspectedAt.slice(0, 16) : ""} />