import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, User, Pagination, Spinner, Popover, PopoverTrigger, PopoverContent, Button, CheckboxGroup, Checkbox, Drawer, DrawerContent, DrawerHeader, DrawerBody, DrawerFooter, useDisclosure, Chip, } from "@heroui/react" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useAsyncList } from "@react-stately/data" import type { components } from "../../types/saturday" import { saturdayClient } from "../../utils/client" import EventDetail, { EventStatusChip, type EventDetailRef } from "./EventDetail" import dayjs from "dayjs" import { EventStatus, UserEventStatus, type RepairEvent } from "../../types/event" import { makeLogtoClient } from "../../utils/auth" import type { PublicMember } from "../../store/member" import type { UserInfoResponse } from "@logto/browser" import { getAvailableEventActions, type EventAction, type IdentityContext } from "./EventAction" import { ExportExcelModal } from "./ExportEventDialog" type PublicEvent = components["schemas"]["PublicEvent"] export const EyeIcon = (props) => { return ( ) } function CheckboxPopover(props: { value: string[] onValueChange: (value: string[]) => void }) { return ( { UserEventStatus.map((status) => { return ( ) }) } ) } function TicketDetailDrawer(props: { event: PublicEvent identity: IdentityContext isOpen: boolean onEventUpdated: (event: RepairEvent) => void onOpenChange: (isOpen: boolean) => void onClose: () => void onDelete: () => void onEdit: () => void }) { const { isOpen, onOpenChange, onClose } = props const [isLoading, setIsLoading] = useState("") const eventDetailRef = useRef(null) const [availableActions, setAvailableActions] = useState([]) useEffect(() => { if (!props.event || !props.identity?.member || !props.identity?.userInfo?.roles) { return } setAvailableActions(getAvailableEventActions(props.event, props.identity)) }, [props.event, props.identity]) const onEventUpdated = async (event: RepairEvent) => { props.onEventUpdated(event) const res = await eventDetailRef.current?.refresh() console.log("onEventUpdated", res) if (!res || !props.identity?.member || !props.identity?.userInfo?.roles) { return } setAvailableActions(getAvailableEventActions(res, props.identity)) } return ( 维修详情 {isLoading && {isLoading}} { event => ( { availableActions?.map((action) => { return ( { setIsLoading(action) }} > ) }) || <>> } ) } 关闭 ) } export const validateRepairRole = (roles: string[]) => { const acceptableRoles = ["repair admin", "repair member"] return roles.some(role => acceptableRoles.includes(role.toLowerCase())) } export default function App() { const [isLoading, setIsLoading] = useState(true) // Initialize state from URL query params const getInitialPage = () => { const params = new URLSearchParams(window.location.search) const pageParam = params.get('page') return pageParam ? parseInt(pageParam, 10) : 1 } const getInitialStatusFilter = () => { const params = new URLSearchParams(window.location.search) const statusParam = params.get('status') if (statusParam) { return statusParam.split(',').filter(Boolean) } return UserEventStatus.filter(v => v.status !== EventStatus.cancelled).map(v => v.status) } const [page, setPage] = useState(getInitialPage()) const rowsPerPage = 10 const [totalCount, setTotalCount] = useState(0) const [statusFilter, setStatusFilter] = useState(getInitialStatusFilter()) const { isOpen, onOpen, onOpenChange } = useDisclosure() const [userInfo, setUserInfo] = useState() const [currentMember, setCurrentMember] = useState() const [token, setToken] = useState() const [errorMessage, setErrorMessage] = useState("") useEffect(() => { const check = async () => { const adminPath = "/repair/admin" const authenticated = await makeLogtoClient().isAuthenticated() if (!authenticated) { window.location.href = `/repair/login-hint?redirectUrl=${adminPath}` return } const res = await makeLogtoClient().getIdTokenClaims() const token = await makeLogtoClient().getAccessToken() setToken(token) const hasRole = validateRepairRole(res.roles) if (!hasRole) { window.location.href = `/repair/login-hint?redirectUrl=${adminPath}` return } setUserInfo(res) const { data } = await saturdayClient.GET("/member", { params: { header: { Authorization: `Bearer ${token}`, }, }, }) setCurrentMember(data) } check() }, []) // Handle eventid query parameter to auto-open event detail useEffect(() => { const loadEventFromUrl = async () => { if (!token) return // Wait for authentication const params = new URLSearchParams(window.location.search) const eventId = params.get('eventid') if (eventId) { try { const { data, error } = await saturdayClient.GET("/events/{eventId}", { params: { path: { eventId: eventId, }, }, }) if (error || !data) { setErrorMessage(`无法找到工单 #${eventId},该工单可能不存在或已被删除`) } else { setActiveEvent(data as PublicEvent) onOpen() } } catch (err) { setErrorMessage(`加载工单 #${eventId} 时出错`) } } } loadEventFromUrl() }, [token]) const list = useAsyncList({ async load() { setIsLoading(true) const offset = (page - 1) * rowsPerPage const { data, response } = await saturdayClient.GET("/events", { params: { query: { order: "DESC", offset: offset, limit: rowsPerPage, status: statusFilter.length > 0 ? statusFilter : null, }, }, }) // Extract total count from response headers const totalCountHeader = response.headers.get("X-Total-Count") if (totalCountHeader) { setTotalCount(parseInt(totalCountHeader, 10)) } setIsLoading(false) return { items: data, } }, async sort({ items, sortDescriptor }) { return { items: items.sort((a, b) => { const first = a[sortDescriptor.column] const second = b[sortDescriptor.column] let cmp = (parseInt(first) || first) < (parseInt(second) || second) ? -1 : 1 if (sortDescriptor.direction === "descending") { cmp *= -1 } return cmp }), } }, }) // Items are now paginated and filtered by the server const items = useMemo(() => { return list.items }, [list.items]) const pages = useMemo(() => { return Math.ceil(totalCount / rowsPerPage) }, [totalCount, rowsPerPage]) // Update URL query params when page or statusFilter changes useEffect(() => { const params = new URLSearchParams(window.location.search) params.set('page', page.toString()) if (statusFilter.length > 0) { params.set('status', statusFilter.join(',')) } else { params.delete('status') } const newUrl = `${window.location.pathname}?${params.toString()}` window.history.replaceState({}, '', newUrl) }, [page, statusFilter]) useEffect(() => { setPage(1) list.reload() }, [statusFilter]) useEffect(() => { list.reload() }, [page]) const columns: { key: string label: string allowSorting?: boolean content?: JSX.Element }[] = [ { key: "eventId", label: "单号", }, { key: "problem", label: "问题描述", }, { key: "model", label: "型号", }, { key: "size", label: "工作量", }, { key: "memberId", label: "处理人", }, { key: "gmtCreate", label: "创建时间", }, { key: "status", label: "状态", content: ( 状态 ), }, { key: "actions", label: "操作", }, ] const [activeEvent, setActiveEvent] = useState() const onOpenEventDetail = (event: PublicEvent) => { setActiveEvent(event) onOpen() // Update URL with eventid const params = new URLSearchParams(window.location.search) params.set('eventid', event.eventId) const newUrl = `${window.location.pathname}?${params.toString()}` window.history.replaceState({}, '', newUrl) } const handleDrawerOpenChange = (isOpen: boolean) => { onOpenChange() // Remove eventid from URL when drawer is closed if (!isOpen) { const params = new URLSearchParams(window.location.search) params.delete('eventid') const newUrl = `${window.location.pathname}?${params.toString()}` window.history.replaceState({}, '', newUrl) } } const MobileEventCard = ({ event }: { event: PublicEvent }) => ( onOpenEventDetail(event)}> {event.problem} #{event.eventId} { dayjs(event.gmtCreate).format("YYYY-MM-DD HH:mm") } {event.model && ( {event.model} )} { event.size && size:{event.size}} {event.member && ( )} ) const renderCell = useCallback((event: PublicEvent, columnKey: string | number) => { const cellValue = event[columnKey] switch (columnKey) { case "problem": return ( {cellValue} ) case "memberId": return ( event.member ? ( {event.member.alias} ) : <>> ) case "size": return ( cellValue ? {"size:" + cellValue} : <>> ) case "gmtCreate": return ( {dayjs(cellValue).format("YYYY-MM-DD HH:mm")} ) case "status": return EventStatusChip({ status: cellValue, size: "sm", }) case "actions": return ( onOpenEventDetail(event)} size="sm" isIconOnly variant="light"> ) default: return cellValue } }, []) return ( 维修管理 { userInfo?.roles?.find(v => v.toLowerCase() == "repair admin") ? : <>> } {/* Mobile Cards Layout */} {/* Filter Section for Mobile */} 筛选状态: {isLoading ? ( ) : ( {items.map(event => ( ))} {items.length === 0 && ( 暂无维修记录 )} )} {/* Mobile Pagination */} setPage(page)} /> {/* Desktop Table Layout */} setPage(page)} /> )} > {column => {column.label}}>} }> {item => ( {columnKey => {renderCell(item, columnKey)}} )} handleDrawerOpenChange(false)} onDelete={() => {}} onEdit={() => {}} > {/* Error Message Display */} {errorMessage && ( {errorMessage} setErrorMessage("")} className="text-red-400 hover:text-red-600 flex-shrink-0" > )} ) }
{errorMessage}