mirror of
https://github.com/m1ngsama/FUJI.git
synced 2025-12-24 10:51:27 +00:00
add event actions
This commit is contained in:
parent
bd38b8ed2b
commit
33d93d0347
5 changed files with 487 additions and 203 deletions
288
src/pages/repair/EventAction.tsx
Normal file
288
src/pages/repair/EventAction.tsx
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
import type { UserInfoResponse } from "@logto/browser"
|
||||||
|
import type { PublicMember } from "../../store/member"
|
||||||
|
import { EventStatus, type PublicEvent } from "../../types/event"
|
||||||
|
import { saturdayApiBaseUrl } from "../../utils/client"
|
||||||
|
import { Button, Form, Select, SelectItem, Textarea } from "@heroui/react"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
export type IdentityContext = {
|
||||||
|
member: PublicMember
|
||||||
|
userInfo: UserInfoResponse
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RepairRole {
|
||||||
|
repairAdmin = "repair admin",
|
||||||
|
repairMember = "repair member",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventActionProps = {
|
||||||
|
event: PublicEvent
|
||||||
|
identityContext: IdentityContext
|
||||||
|
isLoading?: string
|
||||||
|
onUpdated: (event: PublicEvent) => void
|
||||||
|
onLoading: (loadingAction?: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventSizeOptions: {
|
||||||
|
size: string
|
||||||
|
description?: string
|
||||||
|
}[] = [
|
||||||
|
{ size: "xs", description: "无需工具,仅简单排查或软件层级操作" },
|
||||||
|
{ size: "s", description: "简单拆装部件,操作快,风险低" },
|
||||||
|
{ size: "m", description: "需基本工具、一定技术判断,时间较长" },
|
||||||
|
{ size: "l", description: "较复杂的拆装和测试流程,需熟练技能、多人协作可能" },
|
||||||
|
{ size: "xl", description: "工作量极大,涉及多个设备,需团队作业和详细记录" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const EventActionCommitForm = (props: {
|
||||||
|
formData: {
|
||||||
|
size: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
setFormData: (data: {
|
||||||
|
size: string
|
||||||
|
description: string
|
||||||
|
}) => void
|
||||||
|
}) => {
|
||||||
|
const { formData, setFormData } = props
|
||||||
|
return (
|
||||||
|
<Form>
|
||||||
|
<Select
|
||||||
|
items={EventSizeOptions}
|
||||||
|
label="维修难度"
|
||||||
|
size="sm"
|
||||||
|
value={formData.size}
|
||||||
|
onChange={(value) => {
|
||||||
|
setFormData({ ...formData, size: value.target.value.split(",")[0] })
|
||||||
|
}}
|
||||||
|
placeholder="请选择维修难度"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
size => (
|
||||||
|
<SelectItem key={size.size} textValue={"size:" + size.size}>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-small">{size.size}</span>
|
||||||
|
<span className="text-tiny text-default-400">{size.description}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
<Textarea
|
||||||
|
label="维修描述"
|
||||||
|
placeholder="请输入维修描述"
|
||||||
|
errorMessage="维修描述不能为空"
|
||||||
|
required
|
||||||
|
name="description"
|
||||||
|
value={formData.description || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
setFormData({ ...formData, description: e.target.value })
|
||||||
|
}}
|
||||||
|
isRequired
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EventActionCommit = (props: EventActionProps) => {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
size: "",
|
||||||
|
description: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
props.onLoading("commit")
|
||||||
|
const res = await fetch(`${saturdayApiBaseUrl}/member/events/${props.event.eventId}/commit`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${props.identityContext.token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
size: formData.size,
|
||||||
|
problem: formData.description,
|
||||||
|
}),
|
||||||
|
}).then(res => res.json())
|
||||||
|
props.onLoading()
|
||||||
|
return props.onUpdated(res)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<EventActionCommitForm
|
||||||
|
formData={formData}
|
||||||
|
setFormData={setFormData}
|
||||||
|
>
|
||||||
|
</EventActionCommitForm>
|
||||||
|
<Button
|
||||||
|
variant="flat"
|
||||||
|
isLoading={props.isLoading === "commit"}
|
||||||
|
onPress={() => onSubmit()}
|
||||||
|
>
|
||||||
|
提交
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export const EventActionAlterCommit = (props: EventActionProps) => {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
size: props.event.size,
|
||||||
|
description: props.event.problem,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
props.onLoading("alterCommit")
|
||||||
|
const res = await fetch(`${saturdayApiBaseUrl}/member/events/${props.event.eventId}/commit`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${props.identityContext.token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
size: formData.size,
|
||||||
|
problem: formData.description,
|
||||||
|
}),
|
||||||
|
}).then(res => res.json())
|
||||||
|
props.onLoading()
|
||||||
|
return props.onUpdated(res)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<EventActionCommitForm
|
||||||
|
formData={formData}
|
||||||
|
setFormData={setFormData}
|
||||||
|
>
|
||||||
|
</EventActionCommitForm>
|
||||||
|
<Button
|
||||||
|
variant="flat"
|
||||||
|
isLoading={props.isLoading === "commit"}
|
||||||
|
onPress={() => onSubmit()}
|
||||||
|
>
|
||||||
|
修改提交
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommonHandler = () => Promise<unknown>
|
||||||
|
type JsxHandler = (props: EventActionProps) => JSX.Element
|
||||||
|
export type EventAction = {
|
||||||
|
action: string
|
||||||
|
label?: string
|
||||||
|
handler?: CommonHandler
|
||||||
|
jsxHandler: JsxHandler
|
||||||
|
}
|
||||||
|
export const getAvailableEventActions = (event: PublicEvent, identityContext: IdentityContext) => {
|
||||||
|
console.log("getting event actions", event, identityContext)
|
||||||
|
const actions: EventAction[] = []
|
||||||
|
|
||||||
|
const makeCommonJsxHandler = (action: Omit<EventAction, "jsxHandler">) => {
|
||||||
|
return (props: EventActionProps) => {
|
||||||
|
const onAction = async (action: {
|
||||||
|
action: string
|
||||||
|
handler?: CommonHandler
|
||||||
|
}) => {
|
||||||
|
props.onLoading(action.action)
|
||||||
|
if (action.handler) {
|
||||||
|
const res = await action.handler()
|
||||||
|
props.onUpdated(res as PublicEvent)
|
||||||
|
}
|
||||||
|
props.onLoading()
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Button
|
||||||
|
variant="flat"
|
||||||
|
isLoading={props.isLoading === action.action}
|
||||||
|
isDisabled={props.isLoading}
|
||||||
|
onPress={() => onAction(action)}
|
||||||
|
>
|
||||||
|
{action.label ?? action.action}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.status == EventStatus.open) {
|
||||||
|
actions.push({
|
||||||
|
action: "accept",
|
||||||
|
jsxHandler: makeCommonJsxHandler({
|
||||||
|
action: "accept",
|
||||||
|
label: "接受",
|
||||||
|
handler: async () => {
|
||||||
|
return await fetch(`${saturdayApiBaseUrl}/member/events/${event.eventId}/accept`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${identityContext.token}`,
|
||||||
|
},
|
||||||
|
}).then(res => res.json())
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else if (event.status == EventStatus.accepted && event.member?.memberId == identityContext.member.memberId) {
|
||||||
|
actions.push({
|
||||||
|
action: "commit",
|
||||||
|
jsxHandler: EventActionCommit,
|
||||||
|
})
|
||||||
|
actions.push({
|
||||||
|
action: "drop",
|
||||||
|
jsxHandler: makeCommonJsxHandler({
|
||||||
|
action: "drop",
|
||||||
|
label: "放弃",
|
||||||
|
handler: async () => {
|
||||||
|
return await fetch(`${saturdayApiBaseUrl}/member/events/${event.eventId}/accept`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${identityContext.token}`,
|
||||||
|
},
|
||||||
|
}).then(res => res.json())
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else if (event.status == EventStatus.committed) {
|
||||||
|
if (event.member?.memberId == identityContext.member.memberId) {
|
||||||
|
actions.push({
|
||||||
|
action: "alterCommit",
|
||||||
|
jsxHandler: EventActionAlterCommit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (identityContext.userInfo.roles.find(role => role.toLocaleLowerCase() == RepairRole.repairAdmin)) {
|
||||||
|
actions.push({
|
||||||
|
action: "close",
|
||||||
|
jsxHandler: makeCommonJsxHandler({
|
||||||
|
action: "close",
|
||||||
|
label: "关闭",
|
||||||
|
handler: async () => {
|
||||||
|
return await fetch(`${saturdayApiBaseUrl}/events/${event.eventId}/close`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${identityContext.token}`,
|
||||||
|
},
|
||||||
|
}).then(res => res.json())
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
actions.push({
|
||||||
|
action: "reject",
|
||||||
|
jsxHandler: makeCommonJsxHandler({
|
||||||
|
action: "rejectCommit",
|
||||||
|
label: "退回",
|
||||||
|
handler: async () => {
|
||||||
|
return await fetch(`${saturdayApiBaseUrl}/events/${event.eventId}/commit`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${identityContext.token}`,
|
||||||
|
},
|
||||||
|
}).then(res => res.json())
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from "react"
|
import { forwardRef, useEffect, useImperativeHandle, useState } from "react"
|
||||||
import type { components } from "../../types/saturday"
|
import type { components } from "../../types/saturday"
|
||||||
import { saturdayClient } from "../../utils/client"
|
import { saturdayClient } from "../../utils/client"
|
||||||
import { Textarea, Input, Chip } from "@heroui/react"
|
import { Textarea, Input, Chip } from "@heroui/react"
|
||||||
|
|
@ -15,8 +15,6 @@ function EventLogItem(props: {
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="py-1 flex items-center">
|
<div className="py-1 flex items-center">
|
||||||
{/* <div className="mr-4 h-10 bg-red-400 flex flex-col items-center gap-2">
|
|
||||||
</div> */}
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="mr-4">
|
<div className="mr-4">
|
||||||
|
|
@ -55,9 +53,9 @@ export function EventStatusChip(props: {
|
||||||
case EventStatus.open:
|
case EventStatus.open:
|
||||||
return <Chip size={size}>未开始</Chip>
|
return <Chip size={size}>未开始</Chip>
|
||||||
case EventStatus.accepted:
|
case EventStatus.accepted:
|
||||||
return <Chip size={size}>维修中</Chip>
|
|
||||||
case EventStatus.committed:
|
|
||||||
return <Chip size={size} color="primary">维修中</Chip>
|
return <Chip size={size} color="primary">维修中</Chip>
|
||||||
|
case EventStatus.committed:
|
||||||
|
return <Chip size={size} color="secondary">待审核</Chip>
|
||||||
case EventStatus.closed:
|
case EventStatus.closed:
|
||||||
return <Chip size={size} color="success">已完成</Chip>
|
return <Chip size={size} color="success">已完成</Chip>
|
||||||
case EventStatus.cancelled:
|
case EventStatus.cancelled:
|
||||||
|
|
@ -95,11 +93,16 @@ const filterEventLog = (event: PublicEvent) => {
|
||||||
}
|
}
|
||||||
return filteredLogs
|
return filteredLogs
|
||||||
}
|
}
|
||||||
|
export type EventDetailRef = {
|
||||||
export default function EventDetail(props: {
|
refresh: () => Promise<PublicEvent | undefined>
|
||||||
|
}
|
||||||
|
const EventDetail = forwardRef<EventDetailRef, {
|
||||||
eventId?: number
|
eventId?: number
|
||||||
}) {
|
onRefresh?: () => void
|
||||||
|
action?: React.ReactNode
|
||||||
|
}>((props, ref) => {
|
||||||
const [event, setEvent] = useState<PublicEvent | undefined>()
|
const [event, setEvent] = useState<PublicEvent | undefined>()
|
||||||
|
|
||||||
const fetchAndSetEvent = async (eventId: number) => {
|
const fetchAndSetEvent = async (eventId: number) => {
|
||||||
const { data } = await saturdayClient.GET("/events/{EventId}", {
|
const { data } = await saturdayClient.GET("/events/{EventId}", {
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -109,21 +112,33 @@ export default function EventDetail(props: {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
setEvent(data)
|
setEvent(data)
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
|
||||||
|
const refresh = async () => {
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
const eventId = props.eventId ?? url.searchParams.get("eventId")
|
const eventId = props.eventId ?? url.searchParams.get("eventId")
|
||||||
if (!eventId) {
|
console.log("refresh eventId", eventId)
|
||||||
return
|
if (eventId) {
|
||||||
|
return await fetchAndSetEvent(eventId as unknown as number)
|
||||||
}
|
}
|
||||||
fetchAndSetEvent(eventId as unknown as number)
|
}
|
||||||
|
|
||||||
|
// 初次加载
|
||||||
|
useEffect(() => {
|
||||||
|
refresh()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// 暴露给父组件的方法
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
refresh,
|
||||||
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
event
|
event
|
||||||
? (
|
? (
|
||||||
<section className="box-border mb-24">
|
<section className="box-border">
|
||||||
<div className="mt-8">
|
<div className="">
|
||||||
<h2 className="text-2xl font-bold">维修详情</h2>
|
<h2 className="text-2xl font-bold">维修详情</h2>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<span>
|
<span>
|
||||||
|
|
@ -132,7 +147,7 @@ export default function EventDetail(props: {
|
||||||
<EventStatusChip status={event.status}></EventStatusChip>
|
<EventStatusChip status={event.status}></EventStatusChip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="my-8 flex flex-col gap-4">
|
<div className="my-6 flex flex-col gap-4">
|
||||||
<Textarea
|
<Textarea
|
||||||
label="问题描述"
|
label="问题描述"
|
||||||
readOnly
|
readOnly
|
||||||
|
|
@ -163,4 +178,6 @@ export default function EventDetail(props: {
|
||||||
)
|
)
|
||||||
: <div></div>
|
: <div></div>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
|
export default EventDetail
|
||||||
|
|
|
||||||
|
|
@ -20,18 +20,21 @@ import {
|
||||||
DrawerBody,
|
DrawerBody,
|
||||||
DrawerFooter,
|
DrawerFooter,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
|
Chip,
|
||||||
} from "@heroui/react"
|
} from "@heroui/react"
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { useAsyncList } from "@react-stately/data"
|
import { useAsyncList } from "@react-stately/data"
|
||||||
import type { components } from "../../types/saturday"
|
import type { components } from "../../types/saturday"
|
||||||
import { saturdayClient } from "../../utils/client"
|
import { saturdayApiBaseUrl, saturdayClient } from "../../utils/client"
|
||||||
import EventDetail, { EventStatusChip } from "./EventDetail"
|
import EventDetail, { EventStatusChip, type EventDetailRef } from "./EventDetail"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { UserEventStatus } from "../../types/event"
|
import { UserEventStatus } from "../../types/event"
|
||||||
import { makeLogtoClient } from "../../utils/auth"
|
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"
|
||||||
|
|
||||||
type PublicEvent = components["schemas"]["PublicEvent"]
|
type PublicEvent = components["schemas"]["PublicEvent"]
|
||||||
// type EventLog = components["schemas"]["EventLog"]
|
|
||||||
|
|
||||||
export const EyeIcon = (props) => {
|
export const EyeIcon = (props) => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -63,106 +66,10 @@ export const EyeIcon = (props) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteIcon = (props) => {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="1em"
|
|
||||||
role="presentation"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
width="1em"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M17.5 4.98332C14.725 4.70832 11.9333 4.56665 9.15 4.56665C7.5 4.56665 5.85 4.64998 4.2 4.81665L2.5 4.98332"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M7.08331 4.14169L7.26665 3.05002C7.39998 2.25835 7.49998 1.66669 8.90831 1.66669H11.0916C12.5 1.66669 12.6083 2.29169 12.7333 3.05835L12.9166 4.14169"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M15.7084 7.61664L15.1667 16.0083C15.075 17.3166 15 18.3333 12.675 18.3333H7.32502C5.00002 18.3333 4.92502 17.3166 4.83335 16.0083L4.29169 7.61664"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M8.60834 13.75H11.3833"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M7.91669 10.4167H12.0834"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EditIcon = (props) => {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="1em"
|
|
||||||
role="presentation"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
width="1em"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M11.05 3.00002L4.20835 10.2417C3.95002 10.5167 3.70002 11.0584 3.65002 11.4334L3.34169 14.1334C3.23335 15.1084 3.93335 15.775 4.90002 15.6084L7.58335 15.15C7.95835 15.0834 8.48335 14.8084 8.74168 14.525L15.5834 7.28335C16.7667 6.03335 17.3 4.60835 15.4583 2.86668C13.625 1.14168 12.2334 1.75002 11.05 3.00002Z"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeMiterlimit={10}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M9.90833 4.20831C10.2667 6.50831 12.1333 8.26665 14.45 8.49998"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeMiterlimit={10}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M2.5 18.3333H17.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeMiterlimit={10}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
function CheckboxPopover(props: {
|
function CheckboxPopover(props: {
|
||||||
value: string[]
|
value: string[]
|
||||||
onValueChange: (value: string[]) => void
|
onValueChange: (value: string[]) => void
|
||||||
}) {
|
}) {
|
||||||
// const [selectedValues, setSelectedValues] = useState([])
|
|
||||||
|
|
||||||
// const handleSelectionChange = (values) => {
|
|
||||||
// setSelectedValues(values)
|
|
||||||
// }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover placement="bottom">
|
<Popover placement="bottom">
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
|
|
@ -196,23 +103,67 @@ function CheckboxPopover(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
function TicketDetailDrawer(props: {
|
function TicketDetailDrawer(props: {
|
||||||
eventId: number
|
event: PublicEvent
|
||||||
|
identity: IdentityContext
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
|
onEventUpdated: (event: PublicEvent) => void
|
||||||
onOpenChange: (isOpen: boolean) => void
|
onOpenChange: (isOpen: boolean) => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onDelete: () => void
|
onDelete: () => void
|
||||||
onEdit: () => void
|
onEdit: () => void
|
||||||
}) {
|
}) {
|
||||||
const { isOpen, onOpenChange, onClose } = props
|
const { isOpen, onOpenChange, onClose } = props
|
||||||
|
const [isLoading, setIsLoading] = useState("")
|
||||||
|
|
||||||
|
const eventDetailRef = useRef<EventDetailRef>(null)
|
||||||
|
|
||||||
|
const [availableActions, setAvailableActions] = useState<EventAction[]>([])
|
||||||
|
|
||||||
|
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: PublicEvent) => {
|
||||||
|
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 (
|
return (
|
||||||
<Drawer isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Drawer isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
<DrawerHeader>
|
<DrawerHeader>
|
||||||
<h2 className="text-2xl font-bold">维修详情</h2>
|
<h2 className="text-2xl font-bold">维修详情</h2>
|
||||||
|
{isLoading}
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
<DrawerBody>
|
<DrawerBody>
|
||||||
<EventDetail eventId={props.eventId}></EventDetail>
|
<EventDetail ref={eventDetailRef} eventId={props.event?.eventId}></EventDetail>
|
||||||
|
<div className="mb-12 flex flex-col gap-2">
|
||||||
|
{
|
||||||
|
availableActions?.map((action) => {
|
||||||
|
return (
|
||||||
|
<action.jsxHandler
|
||||||
|
key={action.action}
|
||||||
|
event={props.event}
|
||||||
|
isLoading={isLoading}
|
||||||
|
identityContext={props.identity}
|
||||||
|
onUpdated={onEventUpdated}
|
||||||
|
onLoading={(action) => {
|
||||||
|
setIsLoading(action)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
</action.jsxHandler>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</DrawerBody>
|
</DrawerBody>
|
||||||
<DrawerFooter>
|
<DrawerFooter>
|
||||||
<Button variant="flat" onPress={onClose}>
|
<Button variant="flat" onPress={onClose}>
|
||||||
|
|
@ -232,11 +183,12 @@ export const validateRepairRole = (roles: string[]) => {
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const rowsPerPage = 15
|
const rowsPerPage = 10
|
||||||
// const [events, setEvents] = useState<PublicEvent[]>([])
|
|
||||||
const [statusFilter, setStatusFilter] = useState<string[]>([])
|
const [statusFilter, setStatusFilter] = useState<string[]>([])
|
||||||
const { isOpen, onOpen, onOpenChange } = useDisclosure()
|
const { isOpen, onOpen, onOpenChange } = useDisclosure()
|
||||||
// const [userInfo, setUserInfo] = useState<UserInfoResponse>()
|
const [userInfo, setUserInfo] = useState<UserInfoResponse>()
|
||||||
|
const [currentMember, setCurrentMember] = useState<PublicMember>()
|
||||||
|
const [token, setToken] = useState<string>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const check = async () => {
|
const check = async () => {
|
||||||
|
|
@ -247,27 +199,25 @@ export default function App() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const res = await makeLogtoClient().getIdTokenClaims()
|
const res = await makeLogtoClient().getIdTokenClaims()
|
||||||
|
const token = await makeLogtoClient().getAccessToken()
|
||||||
|
setToken(token)
|
||||||
const hasRole = validateRepairRole(res.roles)
|
const hasRole = validateRepairRole(res.roles)
|
||||||
if (!hasRole) {
|
if (!hasRole) {
|
||||||
window.location.href = `/repair/login-hint?redirectUrl=${adminPath}`
|
window.location.href = `/repair/login-hint?redirectUrl=${adminPath}`
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// setUserInfo(res)
|
setUserInfo(res)
|
||||||
|
|
||||||
|
const currentMember = await fetch(`${saturdayApiBaseUrl}/member`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}).then(res => res.json())
|
||||||
|
setCurrentMember(currentMember)
|
||||||
}
|
}
|
||||||
check()
|
check()
|
||||||
}, [])
|
}, [])
|
||||||
// const fetchAndSetEvent = async () => {
|
|
||||||
// const { data } = await saturdayClient.GET("/events", {
|
|
||||||
// params: {
|
|
||||||
// query: {
|
|
||||||
// order: "DESC",
|
|
||||||
// offset: 1,
|
|
||||||
// limit: 1000,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
// setEvents(data)
|
|
||||||
// }
|
|
||||||
|
|
||||||
const list = useAsyncList<PublicEvent>({
|
const list = useAsyncList<PublicEvent>({
|
||||||
async load() {
|
async load() {
|
||||||
|
|
@ -321,11 +271,17 @@ export default function App() {
|
||||||
const pages = useMemo(() => {
|
const pages = useMemo(() => {
|
||||||
return Math.ceil(filteredList.length / rowsPerPage)
|
return Math.ceil(filteredList.length / rowsPerPage)
|
||||||
}, [filteredList, rowsPerPage])
|
}, [filteredList, rowsPerPage])
|
||||||
// useEffect(() => {
|
|
||||||
// fetchAndSetEvent()
|
|
||||||
// }, [])
|
|
||||||
|
|
||||||
const columns = [
|
useEffect(() => {
|
||||||
|
setPage(1)
|
||||||
|
}, [statusFilter])
|
||||||
|
|
||||||
|
const columns: {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
allowSorting?: boolean
|
||||||
|
content?: JSX.Element
|
||||||
|
}[] = [
|
||||||
{
|
{
|
||||||
key: "eventId",
|
key: "eventId",
|
||||||
label: "单号",
|
label: "单号",
|
||||||
|
|
@ -338,6 +294,10 @@ export default function App() {
|
||||||
key: "model",
|
key: "model",
|
||||||
label: "型号",
|
label: "型号",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "size",
|
||||||
|
label: "工作量",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "memberId",
|
key: "memberId",
|
||||||
label: "处理人",
|
label: "处理人",
|
||||||
|
|
@ -362,13 +322,13 @@ export default function App() {
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const [activeEventId, setActiveEventId] = useState<number>()
|
const [activeEvent, setActiveEvent] = useState<PublicEvent>()
|
||||||
const onOpenEventDetail = (eventId: number) => {
|
const onOpenEventDetail = (event: PublicEvent) => {
|
||||||
setActiveEventId(eventId)
|
setActiveEvent(event)
|
||||||
onOpen()
|
onOpen()
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderCell = useCallback((event: PublicEvent, columnKey: string) => {
|
const renderCell = useCallback((event: PublicEvent, columnKey: string | number) => {
|
||||||
const cellValue = event[columnKey]
|
const cellValue = event[columnKey]
|
||||||
|
|
||||||
switch (columnKey) {
|
switch (columnKey) {
|
||||||
|
|
@ -391,6 +351,10 @@ export default function App() {
|
||||||
)
|
)
|
||||||
: <></>
|
: <></>
|
||||||
)
|
)
|
||||||
|
case "size":
|
||||||
|
return (
|
||||||
|
cellValue ? <Chip size="sm">{"size:" + cellValue}</Chip> : <></>
|
||||||
|
)
|
||||||
case "gmtCreate":
|
case "gmtCreate":
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
|
|
@ -400,10 +364,11 @@ export default function App() {
|
||||||
case "status":
|
case "status":
|
||||||
return EventStatusChip({
|
return EventStatusChip({
|
||||||
status: cellValue,
|
status: cellValue,
|
||||||
|
size: "sm",
|
||||||
})
|
})
|
||||||
case "actions":
|
case "actions":
|
||||||
return (
|
return (
|
||||||
<Button onPress={() => onOpenEventDetail(event.eventId)} size="sm" isIconOnly variant="light">
|
<Button onPress={() => onOpenEventDetail(event)} size="sm" isIconOnly variant="light">
|
||||||
<span className="text-lg text-default-400 cursor-pointer active:opacity-50">
|
<span className="text-lg text-default-400 cursor-pointer active:opacity-50">
|
||||||
<EyeIcon />
|
<EyeIcon />
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -417,7 +382,7 @@ export default function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="box-border mb-24">
|
<section className="box-border mb-24">
|
||||||
<div className="section-content mt-8">
|
<div className="section-content mt-6">
|
||||||
<h2 className="text-2xl font-bold">维修管理</h2>
|
<h2 className="text-2xl font-bold">维修管理</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="section-content my-8 flex flex-col gap-4">
|
<div className="section-content my-8 flex flex-col gap-4">
|
||||||
|
|
@ -452,7 +417,15 @@ export default function App() {
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
<TicketDetailDrawer
|
<TicketDetailDrawer
|
||||||
eventId={activeEventId}
|
event={activeEvent}
|
||||||
|
onEventUpdated={list.reload}
|
||||||
|
identity={
|
||||||
|
{
|
||||||
|
member: currentMember,
|
||||||
|
userInfo: userInfo,
|
||||||
|
token: token,
|
||||||
|
}
|
||||||
|
}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { components } from "./saturday"
|
||||||
|
|
||||||
export interface Status {
|
export interface Status {
|
||||||
status: string
|
status: string
|
||||||
text: string
|
text: string
|
||||||
|
|
@ -76,3 +78,5 @@ export const UserEventAction: Action[] = [
|
||||||
icon: "status_cancelled.svg",
|
icon: "status_cancelled.svg",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export type PublicEvent = components["schemas"]["PublicEvent"]
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ import createClient from "openapi-fetch"
|
||||||
import type { paths as saturdayPaths } from "../types/saturday"
|
import type { paths as saturdayPaths } from "../types/saturday"
|
||||||
import { ApiClient } from "./active"
|
import { ApiClient } from "./active"
|
||||||
|
|
||||||
|
export const saturdayApiBaseUrl = import.meta.env.PROD ? "https://api.nbtca.space/v2/" : "/saturday"
|
||||||
|
|
||||||
export const saturdayClient = createClient<saturdayPaths>({
|
export const saturdayClient = createClient<saturdayPaths>({
|
||||||
baseUrl: import.meta.env.PROD ? "https://api.nbtca.space/v2/" : "/saturday",
|
baseUrl: saturdayApiBaseUrl,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const activeClient = new ApiClient({
|
export const activeClient = new ApiClient({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue