From 3c3ebe66e99e15e0e23d8ee35291d3cbdfe0e611 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 08:02:30 +0000 Subject: [PATCH 1/4] feat: add notification preferences UI to repair admin page - Add notification preferences API endpoints to OpenAPI types - Create NotificationPreferences component with toggle switches - Integrate notification preferences into repair admin page - Support GET and PUT operations for notification preferences - Add optimistic UI updates with error handling - Include mobile-responsive design --- src/pages/repair/NotificationPreferences.tsx | 157 +++++++++++++++++++ src/pages/repair/RepairAdmin.tsx | 9 ++ src/types/saturday.d.ts | 136 ++++++++++++++++ 3 files changed, 302 insertions(+) create mode 100644 src/pages/repair/NotificationPreferences.tsx diff --git a/src/pages/repair/NotificationPreferences.tsx b/src/pages/repair/NotificationPreferences.tsx new file mode 100644 index 0000000..852cc6c --- /dev/null +++ b/src/pages/repair/NotificationPreferences.tsx @@ -0,0 +1,157 @@ +import { useEffect, useState } from "react" +import { Card, CardBody, CardHeader, Switch, Spinner, Button } from "@heroui/react" +import { saturdayClient } from "../../utils/client" +import type { components } from "../../types/saturday" + +type NotificationPreferenceItem = components["schemas"]["NotificationPreferenceItem"] +type UpdateNotificationPreferencesInputBody = components["schemas"]["UpdateNotificationPreferencesInputBody"] + +interface NotificationPreferencesProps { + token: string +} + +export default function NotificationPreferences({ token }: NotificationPreferencesProps) { + const [preferences, setPreferences] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [isSaving, setIsSaving] = useState(false) + const [errorMessage, setErrorMessage] = useState("") + const [successMessage, setSuccessMessage] = useState("") + + useEffect(() => { + loadPreferences() + }, [token]) + + const loadPreferences = async () => { + if (!token) return + + setIsLoading(true) + setErrorMessage("") + + try { + const { data, error } = await saturdayClient.GET("/notification-preferences", { + params: { + header: { + Authorization: `Bearer ${token}`, + }, + }, + }) + + if (error || !data) { + setErrorMessage("加载通知偏好设置失败") + return + } + + setPreferences(data) + } catch (err) { + setErrorMessage("加载通知偏好设置时出错") + } finally { + setIsLoading(false) + } + } + + const handleToggle = async (notificationType: string, newValue: boolean) => { + setIsSaving(true) + setErrorMessage("") + setSuccessMessage("") + + // Optimistically update UI + const previousPreferences = [...preferences] + setPreferences(preferences.map(pref => + pref.notificationType === notificationType + ? { ...pref, enabled: newValue } + : pref + )) + + try { + // Create the request body with all preferences + const requestBody: UpdateNotificationPreferencesInputBody = preferences.reduce((acc, pref) => { + const enabled = pref.notificationType === notificationType ? newValue : pref.enabled + acc[pref.notificationType] = enabled + return acc + }, {} as UpdateNotificationPreferencesInputBody) + + const { error } = await saturdayClient.PUT("/notification-preferences", { + params: { + header: { + Authorization: `Bearer ${token}`, + }, + }, + body: requestBody, + }) + + if (error) { + // Revert on error + setPreferences(previousPreferences) + setErrorMessage("更新通知偏好设置失败") + return + } + + setSuccessMessage("通知偏好设置已更新") + // Clear success message after 3 seconds + setTimeout(() => setSuccessMessage(""), 3000) + } catch (err) { + // Revert on error + setPreferences(previousPreferences) + setErrorMessage("更新通知偏好设置时出错") + } finally { + setIsSaving(false) + } + } + + if (isLoading) { + return ( + + + + + + ) + } + + return ( + + +

通知偏好设置

+

管理您希望接收的通知类型

+
+ + {preferences.map((pref) => ( +
+
+

{pref.description}

+

{pref.notificationType}

+
+ handleToggle(pref.notificationType, value)} + isDisabled={isSaving} + size="sm" + /> +
+ ))} + + {/* Messages */} + {errorMessage && ( +
+

{errorMessage}

+
+ )} + + {successMessage && ( +
+

{successMessage}

+
+ )} + + {preferences.length === 0 && !errorMessage && ( +
+

暂无可用的通知偏好设置

+
+ )} +
+
+ ) +} diff --git a/src/pages/repair/RepairAdmin.tsx b/src/pages/repair/RepairAdmin.tsx index 0054e79..1d50ff5 100644 --- a/src/pages/repair/RepairAdmin.tsx +++ b/src/pages/repair/RepairAdmin.tsx @@ -34,6 +34,7 @@ import type { PublicMember } from "../../store/member" import type { UserInfoResponse } from "@logto/browser" import { getAvailableEventActions, type EventAction, type IdentityContext } from "./EventAction" import { ExportExcelModal } from "./ExportEventDialog" +import NotificationPreferences from "./NotificationPreferences" type PublicEvent = components["schemas"]["PublicEvent"] @@ -531,6 +532,14 @@ export default function App() { : <> } + + {/* Notification Preferences Section */} + {token && ( +
+ +
+ )} +
{/* Mobile Cards Layout */}
diff --git a/src/types/saturday.d.ts b/src/types/saturday.d.ts index c831254..e16a694 100644 --- a/src/types/saturday.d.ts +++ b/src/types/saturday.d.ts @@ -403,6 +403,24 @@ export interface paths { patch?: never trace?: never } + "/notification-preferences": { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Get notification preferences */ + get: operations["get-notification-preferences"] + /** Update notification preferences */ + put: operations["update-notification-preferences"] + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } "/ping": { parameters: { query?: never @@ -664,6 +682,23 @@ export interface components { Int64: number Valid: boolean } + "NotificationPreferenceItem": { + /** + * @description Description of the notification type + * @example 通知所有新创建的维修工单 + */ + description: string + /** + * @description Whether this notification type is enabled + * @example true + */ + enabled: boolean + /** + * @description Notification type identifier + * @example new_event_created + */ + notificationType: string + } "PingResponse": { /** * Format: uri @@ -732,6 +767,29 @@ export interface components { qq?: string size?: string } + "UpdateNotificationPreferencesInputBody": { + /** + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/UpdateNotificationPreferencesInputBody.json + */ + readonly $schema?: string + /** + * @description Enable/disable notifications for events assigned to me + * @example true + */ + event_assigned_to_me: boolean + /** + * @description Enable/disable notifications for event status changes + * @example true + */ + event_status_changed: boolean + /** + * @description Enable/disable notifications for new events created + * @example true + */ + new_event_created: boolean + } "UpdateMemberAvatarInputBody": { /** * Format: uri @@ -2108,6 +2166,84 @@ export interface operations { } } } + "get-notification-preferences": { + parameters: { + query?: never + header?: { + /** @description Bearer token or JWT token */ + Authorization?: string + } + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description OK */ + 200: { + headers: { + "X-Limit"?: number | null + "X-Offset"?: number | null + "X-Page"?: number | null + "X-Total-Count"?: number | null + "X-Total-Pages"?: number | null + [name: string]: unknown + } + content: { + "application/json": components["schemas"]["NotificationPreferenceItem"][] | null + } + } + /** @description Error */ + default: { + headers: { + [name: string]: unknown + } + content: { + "application/problem+json": components["schemas"]["ErrorModel"] + } + } + } + } + "update-notification-preferences": { + parameters: { + query?: never + header?: { + /** @description Bearer token or JWT token */ + Authorization?: string + } + path?: never + cookie?: never + } + requestBody: { + content: { + "application/json": components["schemas"]["UpdateNotificationPreferencesInputBody"] + } + } + responses: { + /** @description OK */ + 200: { + headers: { + "X-Limit"?: number | null + "X-Offset"?: number | null + "X-Page"?: number | null + "X-Total-Count"?: number | null + "X-Total-Pages"?: number | null + [name: string]: unknown + } + content: { + "application/json": string + } + } + /** @description Error */ + default: { + headers: { + [name: string]: unknown + } + content: { + "application/problem+json": components["schemas"]["ErrorModel"] + } + } + } + } "ping": { parameters: { query?: never From 450d3392379f27f9f8df1198324e350a5ab2b1dc Mon Sep 17 00:00:00 2001 From: Clas Wen Date: Sun, 23 Nov 2025 16:03:37 +0800 Subject: [PATCH 2/4] update api defination --- src/types/saturday.d.ts | 1189 +++++++++++++++++++-------------------- 1 file changed, 587 insertions(+), 602 deletions(-) diff --git a/src/types/saturday.d.ts b/src/types/saturday.d.ts index e16a694..c79c30f 100644 --- a/src/types/saturday.d.ts +++ b/src/types/saturday.d.ts @@ -298,6 +298,24 @@ export interface paths { patch: operations["alter-commit-event"] trace?: never } + "/member/notification-preferences": { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Get member notification preferences */ + get: operations["get-notification-preferences"] + /** Update member notification preferences */ + put: operations["update-notification-preferences"] + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } "/member/token/logto": { parameters: { query?: never @@ -403,24 +421,6 @@ export interface paths { patch?: never trace?: never } - "/notification-preferences": { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** Get notification preferences */ - get: operations["get-notification-preferences"] - /** Update notification preferences */ - put: operations["update-notification-preferences"] - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } "/ping": { parameters: { query?: never @@ -444,10 +444,10 @@ export interface components { schemas: { "ActivateMemberRequest": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/ActivateMemberRequest.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/ActivateMemberRequest.json + */ readonly $schema?: string MemberId: string alias: string @@ -458,29 +458,29 @@ export interface components { } "AlterCommitEventInputBody": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/AlterCommitEventInputBody.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/AlterCommitEventInputBody.json + */ readonly $schema?: string content: string size?: string } "Bind-member-logto-idRequest": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/Bind-member-logto-idRequest.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/Bind-member-logto-idRequest.json + */ readonly $schema?: string password: string } "ClientTokenResponse": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/ClientTokenResponse.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/ClientTokenResponse.json + */ readonly $schema?: string /** Format: int64 */ clientId: number @@ -492,38 +492,38 @@ export interface components { } "CommitEventInputBody": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/CommitEventInputBody.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/CommitEventInputBody.json + */ readonly $schema?: string content: string size?: string } "Create-token-via-wechatRequest": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/Create-token-via-wechatRequest.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/Create-token-via-wechatRequest.json + */ readonly $schema?: string code: string } "Create-tokenRequest": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/Create-tokenRequest.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/Create-tokenRequest.json + */ readonly $schema?: string password: string } "CreateClientEventInputBody": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/CreateClientEventInputBody.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/CreateClientEventInputBody.json + */ readonly $schema?: string contactPreference?: string model?: string @@ -533,10 +533,10 @@ export interface components { } "CreateMemberRequest": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/CreateMemberRequest.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/CreateMemberRequest.json + */ readonly $schema?: string alias: string avatar: string @@ -551,10 +551,10 @@ export interface components { } "CreateMemberTokenResponse": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/CreateMemberTokenResponse.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/CreateMemberTokenResponse.json + */ readonly $schema?: string alias: string avatar: string @@ -565,6 +565,7 @@ export interface components { logtoId: string memberId: string name: string + notificationPreferences: components["schemas"]["NotificationPreferences"] phone: string profile: string qq: string @@ -582,49 +583,49 @@ export interface components { } "ErrorModel": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/ErrorModel.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/ErrorModel.json + */ readonly $schema?: string /** - * @description A human-readable explanation specific to this occurrence of the problem. - * @example Property foo is required but is missing. - */ + * @description A human-readable explanation specific to this occurrence of the problem. + * @example Property foo is required but is missing. + */ detail?: string /** @description Optional list of individual error details */ errors?: components["schemas"]["ErrorDetail"][] | null /** - * Format: uri - * @description A URI reference that identifies the specific occurrence of the problem. - * @example https://example.com/error-log/abc123 - */ + * Format: uri + * @description A URI reference that identifies the specific occurrence of the problem. + * @example https://example.com/error-log/abc123 + */ instance?: string /** - * Format: int64 - * @description HTTP status code - * @example 400 - */ + * Format: int64 + * @description HTTP status code + * @example 400 + */ status?: number /** - * @description A short, human-readable summary of the problem type. This value should not change between occurrences of the error. - * @example Bad Request - */ + * @description A short, human-readable summary of the problem type. This value should not change between occurrences of the error. + * @example Bad Request + */ title?: string /** - * Format: uri - * @description A URI reference to human-readable documentation for the error. - * @default about:blank - * @example https://example.com/errors/example - */ + * Format: uri + * @description A URI reference to human-readable documentation for the error. + * @default about:blank + * @example https://example.com/errors/example + */ type: string } "Event": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/Event.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/Event.json + */ readonly $schema?: string /** Format: int64 */ clientId: number @@ -655,12 +656,16 @@ export interface components { logId: number memberId: string } + "Item": { + enabled: boolean + notificationType: string + } "Member": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/Member.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/Member.json + */ readonly $schema?: string alias: string avatar: string @@ -671,53 +676,47 @@ export interface components { logtoId: string memberId: string name: string + notificationPreferences: components["schemas"]["NotificationPreferences"] phone: string profile: string qq: string role: string section: string } + "NotificationPreferenceItem": { + description: string + enabled: boolean + notificationType: string + } + "NotificationPreferences": { + event_assigned_to_me: boolean + event_status_changed: boolean + new_event_created: boolean + } "NullInt64": { /** Format: int64 */ Int64: number Valid: boolean } - "NotificationPreferenceItem": { - /** - * @description Description of the notification type - * @example 通知所有新创建的维修工单 - */ - description: string - /** - * @description Whether this notification type is enabled - * @example true - */ - enabled: boolean - /** - * @description Notification type identifier - * @example new_event_created - */ - notificationType: string - } "PingResponse": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/PingResponse.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/PingResponse.json + */ readonly $schema?: string /** - * @description Ping message - * @example ping - */ + * @description Ping message + * @example ping + */ message: string } "PublicEvent": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/PublicEvent.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/PublicEvent.json + */ readonly $schema?: string /** Format: int64 */ clientId: number @@ -739,10 +738,10 @@ export interface components { } "PublicMember": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/PublicMember.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/PublicMember.json + */ readonly $schema?: string alias: string avatar: string @@ -755,10 +754,10 @@ export interface components { } "UpdateClientEventInputBody": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/UpdateClientEventInputBody.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/UpdateClientEventInputBody.json + */ readonly $schema?: string contactPreference?: string model?: string @@ -767,45 +766,22 @@ export interface components { qq?: string size?: string } - "UpdateNotificationPreferencesInputBody": { - /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/UpdateNotificationPreferencesInputBody.json - */ - readonly $schema?: string - /** - * @description Enable/disable notifications for events assigned to me - * @example true - */ - event_assigned_to_me: boolean - /** - * @description Enable/disable notifications for event status changes - * @example true - */ - event_status_changed: boolean - /** - * @description Enable/disable notifications for new events created - * @example true - */ - new_event_created: boolean - } "UpdateMemberAvatarInputBody": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/UpdateMemberAvatarInputBody.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/UpdateMemberAvatarInputBody.json + */ readonly $schema?: string /** @description Avatar URL */ avatar: string } "UpdateMemberBasicRequest": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/UpdateMemberBasicRequest.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/UpdateMemberBasicRequest.json + */ readonly $schema?: string memberId: string name: string @@ -814,10 +790,10 @@ export interface components { } "UpdateMemberRequest": { /** - * Format: uri - * @description A URL to the JSON Schema for this object. - * @example https://api.nbtca.space/schemas/UpdateMemberRequest.json - */ + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/UpdateMemberRequest.json + */ readonly $schema?: string MemberId: string alias: string @@ -827,6 +803,15 @@ export interface components { profile: string qq: string } + "UpdateNotificationPreferencesInputBody": { + /** + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://api.nbtca.space/schemas/UpdateNotificationPreferencesInputBody.json + */ + readonly $schema?: string + preferences: components["schemas"]["Item"][] | null + } } responses: never parameters: never @@ -881,14 +866,14 @@ export interface operations { parameters: { query?: { /** - * @description Offset - * @example 0 - */ + * @description Offset + * @example 0 + */ offset?: number /** - * @description Limit - * @example 50 - */ + * @description Limit + * @example 50 + */ limit?: number status?: string order?: string @@ -936,9 +921,9 @@ export interface operations { } path: { /** - * @description Event ID - * @example 123 - */ + * @description Event ID + * @example 123 + */ EventId: number } cookie?: never @@ -979,9 +964,9 @@ export interface operations { } path: { /** - * @description Event ID - * @example 123 - */ + * @description Event ID + * @example 123 + */ EventId: number } cookie?: never @@ -1022,9 +1007,9 @@ export interface operations { } path: { /** - * @description Event ID - * @example 123 - */ + * @description Event ID + * @example 123 + */ EventId: number } cookie?: never @@ -1139,14 +1124,14 @@ export interface operations { parameters: { query?: { /** - * @description Offset - * @example 0 - */ + * @description Offset + * @example 0 + */ offset?: number /** - * @description Limit - * @example 50 - */ + * @description Limit + * @example 50 + */ limit?: number status?: string[] | null order?: string @@ -1262,9 +1247,9 @@ export interface operations { } path: { /** - * @description Event ID - * @example 123 - */ + * @description Event ID + * @example 123 + */ EventId: number } cookie?: never @@ -1305,9 +1290,9 @@ export interface operations { } path: { /** - * @description Event ID - * @example 123 - */ + * @description Event ID + * @example 123 + */ EventId: number } cookie?: never @@ -1503,14 +1488,14 @@ export interface operations { parameters: { query?: { /** - * @description Offset - * @example 0 - */ + * @description Offset + * @example 0 + */ offset?: number /** - * @description Limit - * @example 50 - */ + * @description Limit + * @example 50 + */ limit?: number status?: string order?: string @@ -1558,9 +1543,9 @@ export interface operations { } path: { /** - * @description Event ID - * @example 123 - */ + * @description Event ID + * @example 123 + */ EventId: number } cookie?: never @@ -1601,9 +1586,9 @@ export interface operations { } path: { /** - * @description Event ID - * @example 123 - */ + * @description Event ID + * @example 123 + */ EventId: number } cookie?: never @@ -1644,9 +1629,9 @@ export interface operations { } path: { /** - * @description Event ID - * @example 123 - */ + * @description Event ID + * @example 123 + */ EventId: number } cookie?: never @@ -1687,9 +1672,9 @@ export interface operations { } path: { /** - * @description Event ID - * @example 123 - */ + * @description Event ID + * @example 123 + */ EventId: number } cookie?: never @@ -1734,9 +1719,9 @@ export interface operations { } path: { /** - * @description Event ID - * @example 123 - */ + * @description Event ID + * @example 123 + */ EventId: number } cookie?: never @@ -1772,400 +1757,6 @@ export interface operations { } } } - "create-token-via-logto-token": { - parameters: { - query?: never - header?: { - Authorization?: string - } - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description OK */ - 200: { - headers: { - "X-Limit"?: number | null - "X-Offset"?: number | null - "X-Page"?: number | null - "X-Total-Count"?: number | null - "X-Total-Pages"?: number | null - [name: string]: unknown - } - content: { - "application/json": components["schemas"]["CreateMemberTokenResponse"] - } - } - /** @description Error */ - default: { - headers: { - [name: string]: unknown - } - content: { - "application/problem+json": components["schemas"]["ErrorModel"] - } - } - } - } - "get-public-member-by-page": { - parameters: { - query?: { - /** - * @description Offset - * @example 0 - */ - offset?: number - /** - * @description Limit - * @example 50 - */ - limit?: number - } - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description OK */ - 200: { - headers: { - "X-Limit"?: number | null - "X-Offset"?: number | null - "X-Page"?: number | null - "X-Total-Count"?: number | null - "X-Total-Pages"?: number | null - [name: string]: unknown - } - content: { - "application/json": components["schemas"]["PublicMember"][] | null - } - } - /** @description Error */ - default: { - headers: { - [name: string]: unknown - } - content: { - "application/problem+json": components["schemas"]["ErrorModel"] - } - } - } - } - "create-members": { - parameters: { - query?: never - header?: { - /** @description Bearer token or JWT token */ - Authorization?: string - } - path?: never - cookie?: never - } - requestBody: { - content: { - "application/json": components["schemas"]["CreateMemberRequest"][] | null - } - } - responses: { - /** @description OK */ - 200: { - headers: { - "X-Limit"?: number | null - "X-Offset"?: number | null - "X-Page"?: number | null - "X-Total-Count"?: number | null - "X-Total-Pages"?: number | null - [name: string]: unknown - } - content: { - "application/json": components["schemas"]["Member"][] | null - } - } - /** @description Error */ - default: { - headers: { - [name: string]: unknown - } - content: { - "application/problem+json": components["schemas"]["ErrorModel"] - } - } - } - } - "get-members-full": { - parameters: { - query?: { - /** - * @description Offset - * @example 0 - */ - offset?: number - /** - * @description Limit - * @example 50 - */ - limit?: number - } - header?: { - /** @description Bearer token or JWT token */ - Authorization?: string - } - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description OK */ - 200: { - headers: { - "X-Limit"?: number | null - "X-Offset"?: number | null - "X-Page"?: number | null - "X-Total-Count"?: number | null - "X-Total-Pages"?: number | null - [name: string]: unknown - } - content: { - "application/json": components["schemas"]["Member"][] | null - } - } - /** @description Error */ - default: { - headers: { - [name: string]: unknown - } - content: { - "application/problem+json": components["schemas"]["ErrorModel"] - } - } - } - } - "get-public-member": { - parameters: { - query?: never - header?: never - path: { - /** - * @description Name to greet - * @example 2333333333 - */ - MemberId: string - } - cookie?: never - } - requestBody?: never - responses: { - /** @description OK */ - 200: { - headers: { - "X-Limit"?: number | null - "X-Offset"?: number | null - "X-Page"?: number | null - "X-Total-Count"?: number | null - "X-Total-Pages"?: number | null - [name: string]: unknown - } - content: { - "application/json": components["schemas"]["PublicMember"] - } - } - /** @description Error */ - default: { - headers: { - [name: string]: unknown - } - content: { - "application/problem+json": components["schemas"]["ErrorModel"] - } - } - } - } - "create-member": { - parameters: { - query?: never - header?: { - /** @description Bearer token or JWT token */ - Authorization?: string - } - path: { - /** - * @description Member ID - * @example 2333333333 - */ - MemberId: string - } - cookie?: never - } - requestBody: { - content: { - "application/json": components["schemas"]["CreateMemberRequest"] - } - } - responses: { - /** @description OK */ - 200: { - headers: { - "X-Limit"?: number | null - "X-Offset"?: number | null - "X-Page"?: number | null - "X-Total-Count"?: number | null - "X-Total-Pages"?: number | null - [name: string]: unknown - } - content: { - "application/json": components["schemas"]["Member"] - } - } - /** @description Error */ - default: { - headers: { - [name: string]: unknown - } - content: { - "application/problem+json": components["schemas"]["ErrorModel"] - } - } - } - } - "update-member-basic": { - parameters: { - query?: never - header?: { - /** @description Bearer token or JWT token */ - Authorization?: string - } - path: { - /** - * @description Member ID - * @example 2333333333 - */ - MemberId: string - } - cookie?: never - } - requestBody: { - content: { - "application/json": components["schemas"]["UpdateMemberBasicRequest"] - } - } - responses: { - /** @description OK */ - 200: { - headers: { - "X-Limit"?: number | null - "X-Offset"?: number | null - "X-Page"?: number | null - "X-Total-Count"?: number | null - "X-Total-Pages"?: number | null - [name: string]: unknown - } - content: { - "application/json": components["schemas"]["Member"] - } - } - /** @description Error */ - default: { - headers: { - [name: string]: unknown - } - content: { - "application/problem+json": components["schemas"]["ErrorModel"] - } - } - } - } - "bind-member-logto-id": { - parameters: { - query?: never - header?: { - Authorization?: string - } - path: { - /** - * @description Member Id - * @example 2333333333 - */ - MemberId: string - } - cookie?: never - } - requestBody: { - content: { - "application/json": components["schemas"]["Bind-member-logto-idRequest"] - } - } - responses: { - /** @description OK */ - 200: { - headers: { - "X-Limit"?: number | null - "X-Offset"?: number | null - "X-Page"?: number | null - "X-Total-Count"?: number | null - "X-Total-Pages"?: number | null - [name: string]: unknown - } - content: { - "application/json": components["schemas"]["Member"] - } - } - /** @description Error */ - default: { - headers: { - [name: string]: unknown - } - content: { - "application/problem+json": components["schemas"]["ErrorModel"] - } - } - } - } - "create-token": { - parameters: { - query?: never - header?: never - path: { - /** - * @description Member Id - * @example 2333333333 - */ - MemberId: string - } - cookie?: never - } - requestBody: { - content: { - "application/json": components["schemas"]["Create-tokenRequest"] - } - } - responses: { - /** @description OK */ - 200: { - headers: { - "X-Limit"?: number | null - "X-Offset"?: number | null - "X-Page"?: number | null - "X-Total-Count"?: number | null - "X-Total-Pages"?: number | null - [name: string]: unknown - } - content: { - "application/json": components["schemas"]["CreateMemberTokenResponse"] - } - } - /** @description Error */ - default: { - headers: { - [name: string]: unknown - } - content: { - "application/problem+json": components["schemas"]["ErrorModel"] - } - } - } - } "get-notification-preferences": { parameters: { query?: never @@ -2244,6 +1835,400 @@ export interface operations { } } } + "create-token-via-logto-token": { + parameters: { + query?: never + header?: { + Authorization?: string + } + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description OK */ + 200: { + headers: { + "X-Limit"?: number | null + "X-Offset"?: number | null + "X-Page"?: number | null + "X-Total-Count"?: number | null + "X-Total-Pages"?: number | null + [name: string]: unknown + } + content: { + "application/json": components["schemas"]["CreateMemberTokenResponse"] + } + } + /** @description Error */ + default: { + headers: { + [name: string]: unknown + } + content: { + "application/problem+json": components["schemas"]["ErrorModel"] + } + } + } + } + "get-public-member-by-page": { + parameters: { + query?: { + /** + * @description Offset + * @example 0 + */ + offset?: number + /** + * @description Limit + * @example 50 + */ + limit?: number + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description OK */ + 200: { + headers: { + "X-Limit"?: number | null + "X-Offset"?: number | null + "X-Page"?: number | null + "X-Total-Count"?: number | null + "X-Total-Pages"?: number | null + [name: string]: unknown + } + content: { + "application/json": components["schemas"]["PublicMember"][] | null + } + } + /** @description Error */ + default: { + headers: { + [name: string]: unknown + } + content: { + "application/problem+json": components["schemas"]["ErrorModel"] + } + } + } + } + "create-members": { + parameters: { + query?: never + header?: { + /** @description Bearer token or JWT token */ + Authorization?: string + } + path?: never + cookie?: never + } + requestBody: { + content: { + "application/json": components["schemas"]["CreateMemberRequest"][] | null + } + } + responses: { + /** @description OK */ + 200: { + headers: { + "X-Limit"?: number | null + "X-Offset"?: number | null + "X-Page"?: number | null + "X-Total-Count"?: number | null + "X-Total-Pages"?: number | null + [name: string]: unknown + } + content: { + "application/json": components["schemas"]["Member"][] | null + } + } + /** @description Error */ + default: { + headers: { + [name: string]: unknown + } + content: { + "application/problem+json": components["schemas"]["ErrorModel"] + } + } + } + } + "get-members-full": { + parameters: { + query?: { + /** + * @description Offset + * @example 0 + */ + offset?: number + /** + * @description Limit + * @example 50 + */ + limit?: number + } + header?: { + /** @description Bearer token or JWT token */ + Authorization?: string + } + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description OK */ + 200: { + headers: { + "X-Limit"?: number | null + "X-Offset"?: number | null + "X-Page"?: number | null + "X-Total-Count"?: number | null + "X-Total-Pages"?: number | null + [name: string]: unknown + } + content: { + "application/json": components["schemas"]["Member"][] | null + } + } + /** @description Error */ + default: { + headers: { + [name: string]: unknown + } + content: { + "application/problem+json": components["schemas"]["ErrorModel"] + } + } + } + } + "get-public-member": { + parameters: { + query?: never + header?: never + path: { + /** + * @description Name to greet + * @example 2333333333 + */ + MemberId: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description OK */ + 200: { + headers: { + "X-Limit"?: number | null + "X-Offset"?: number | null + "X-Page"?: number | null + "X-Total-Count"?: number | null + "X-Total-Pages"?: number | null + [name: string]: unknown + } + content: { + "application/json": components["schemas"]["PublicMember"] + } + } + /** @description Error */ + default: { + headers: { + [name: string]: unknown + } + content: { + "application/problem+json": components["schemas"]["ErrorModel"] + } + } + } + } + "create-member": { + parameters: { + query?: never + header?: { + /** @description Bearer token or JWT token */ + Authorization?: string + } + path: { + /** + * @description Member ID + * @example 2333333333 + */ + MemberId: string + } + cookie?: never + } + requestBody: { + content: { + "application/json": components["schemas"]["CreateMemberRequest"] + } + } + responses: { + /** @description OK */ + 200: { + headers: { + "X-Limit"?: number | null + "X-Offset"?: number | null + "X-Page"?: number | null + "X-Total-Count"?: number | null + "X-Total-Pages"?: number | null + [name: string]: unknown + } + content: { + "application/json": components["schemas"]["Member"] + } + } + /** @description Error */ + default: { + headers: { + [name: string]: unknown + } + content: { + "application/problem+json": components["schemas"]["ErrorModel"] + } + } + } + } + "update-member-basic": { + parameters: { + query?: never + header?: { + /** @description Bearer token or JWT token */ + Authorization?: string + } + path: { + /** + * @description Member ID + * @example 2333333333 + */ + MemberId: string + } + cookie?: never + } + requestBody: { + content: { + "application/json": components["schemas"]["UpdateMemberBasicRequest"] + } + } + responses: { + /** @description OK */ + 200: { + headers: { + "X-Limit"?: number | null + "X-Offset"?: number | null + "X-Page"?: number | null + "X-Total-Count"?: number | null + "X-Total-Pages"?: number | null + [name: string]: unknown + } + content: { + "application/json": components["schemas"]["Member"] + } + } + /** @description Error */ + default: { + headers: { + [name: string]: unknown + } + content: { + "application/problem+json": components["schemas"]["ErrorModel"] + } + } + } + } + "bind-member-logto-id": { + parameters: { + query?: never + header?: { + Authorization?: string + } + path: { + /** + * @description Member Id + * @example 2333333333 + */ + MemberId: string + } + cookie?: never + } + requestBody: { + content: { + "application/json": components["schemas"]["Bind-member-logto-idRequest"] + } + } + responses: { + /** @description OK */ + 200: { + headers: { + "X-Limit"?: number | null + "X-Offset"?: number | null + "X-Page"?: number | null + "X-Total-Count"?: number | null + "X-Total-Pages"?: number | null + [name: string]: unknown + } + content: { + "application/json": components["schemas"]["Member"] + } + } + /** @description Error */ + default: { + headers: { + [name: string]: unknown + } + content: { + "application/problem+json": components["schemas"]["ErrorModel"] + } + } + } + } + "create-token": { + parameters: { + query?: never + header?: never + path: { + /** + * @description Member Id + * @example 2333333333 + */ + MemberId: string + } + cookie?: never + } + requestBody: { + content: { + "application/json": components["schemas"]["Create-tokenRequest"] + } + } + responses: { + /** @description OK */ + 200: { + headers: { + "X-Limit"?: number | null + "X-Offset"?: number | null + "X-Page"?: number | null + "X-Total-Count"?: number | null + "X-Total-Pages"?: number | null + [name: string]: unknown + } + content: { + "application/json": components["schemas"]["CreateMemberTokenResponse"] + } + } + /** @description Error */ + default: { + headers: { + [name: string]: unknown + } + content: { + "application/problem+json": components["schemas"]["ErrorModel"] + } + } + } + } "ping": { parameters: { query?: never From c6e5a9535a118de9b0595906da892a3db4a727f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 08:05:41 +0000 Subject: [PATCH 3/4] fix: update NotificationPreferences to use new API structure - Update endpoint path from /notification-preferences to /member/notification-preferences - Change PUT request body to use preferences array format - Update types to use Item schema instead of UpdateNotificationPreferencesInputBody --- src/pages/repair/NotificationPreferences.tsx | 21 ++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/pages/repair/NotificationPreferences.tsx b/src/pages/repair/NotificationPreferences.tsx index 852cc6c..aa7e89c 100644 --- a/src/pages/repair/NotificationPreferences.tsx +++ b/src/pages/repair/NotificationPreferences.tsx @@ -4,7 +4,7 @@ import { saturdayClient } from "../../utils/client" import type { components } from "../../types/saturday" type NotificationPreferenceItem = components["schemas"]["NotificationPreferenceItem"] -type UpdateNotificationPreferencesInputBody = components["schemas"]["UpdateNotificationPreferencesInputBody"] +type Item = components["schemas"]["Item"] interface NotificationPreferencesProps { token: string @@ -28,7 +28,7 @@ export default function NotificationPreferences({ token }: NotificationPreferenc setErrorMessage("") try { - const { data, error } = await saturdayClient.GET("/notification-preferences", { + const { data, error } = await saturdayClient.GET("/member/notification-preferences", { params: { header: { Authorization: `Bearer ${token}`, @@ -63,20 +63,21 @@ export default function NotificationPreferences({ token }: NotificationPreferenc )) try { - // Create the request body with all preferences - const requestBody: UpdateNotificationPreferencesInputBody = preferences.reduce((acc, pref) => { - const enabled = pref.notificationType === notificationType ? newValue : pref.enabled - acc[pref.notificationType] = enabled - return acc - }, {} as UpdateNotificationPreferencesInputBody) + // Create the request body with all preferences as an array of Item objects + const preferencesArray: Item[] = preferences.map(pref => ({ + notificationType: pref.notificationType, + enabled: pref.notificationType === notificationType ? newValue : pref.enabled, + })) - const { error } = await saturdayClient.PUT("/notification-preferences", { + const { error } = await saturdayClient.PUT("/member/notification-preferences", { params: { header: { Authorization: `Bearer ${token}`, }, }, - body: requestBody, + body: { + preferences: preferencesArray, + }, }) if (error) { From eb9b68e75346f40b17131b1a88fe4fa84542318d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 08:14:24 +0000 Subject: [PATCH 4/4] refactor: convert notification preferences to modal dialog - Convert NotificationPreferences from inline Card to Modal dialog - Add button next to export button in repair admin header - Load preferences on modal open instead of component mount - Add close button in modal footer - Improve mobile responsiveness with button grouping --- src/pages/repair/NotificationPreferences.tsx | 140 +++++++++++-------- src/pages/repair/RepairAdmin.tsx | 19 +-- 2 files changed, 91 insertions(+), 68 deletions(-) diff --git a/src/pages/repair/NotificationPreferences.tsx b/src/pages/repair/NotificationPreferences.tsx index aa7e89c..d6b426e 100644 --- a/src/pages/repair/NotificationPreferences.tsx +++ b/src/pages/repair/NotificationPreferences.tsx @@ -1,5 +1,14 @@ import { useEffect, useState } from "react" -import { Card, CardBody, CardHeader, Switch, Spinner, Button } from "@heroui/react" +import { + Modal, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, + Switch, + Spinner, + Button, +} from "@heroui/react" import { saturdayClient } from "../../utils/client" import type { components } from "../../types/saturday" @@ -11,15 +20,23 @@ interface NotificationPreferencesProps { } export default function NotificationPreferences({ token }: NotificationPreferencesProps) { + const [isOpen, setIsOpen] = useState(false) const [preferences, setPreferences] = useState([]) - const [isLoading, setIsLoading] = useState(true) + const [isLoading, setIsLoading] = useState(false) const [isSaving, setIsSaving] = useState(false) const [errorMessage, setErrorMessage] = useState("") const [successMessage, setSuccessMessage] = useState("") - useEffect(() => { + const openModal = () => { + setIsOpen(true) loadPreferences() - }, [token]) + } + + const closeModal = () => { + setIsOpen(false) + setErrorMessage("") + setSuccessMessage("") + } const loadPreferences = async () => { if (!token) return @@ -99,60 +116,73 @@ export default function NotificationPreferences({ token }: NotificationPreferenc } } - if (isLoading) { - return ( - - - - - - ) - } - return ( - - -

通知偏好设置

-

管理您希望接收的通知类型

-
- - {preferences.map((pref) => ( -
-
-

{pref.description}

-

{pref.notificationType}

-
- handleToggle(pref.notificationType, value)} - isDisabled={isSaving} - size="sm" - /> -
- ))} + <> + - {/* Messages */} - {errorMessage && ( -
-

{errorMessage}

-
- )} + + + +

通知偏好设置

+

管理您希望接收的通知类型

+
+ + {isLoading ? ( +
+ +
+ ) : ( + <> +
+ {preferences.map((pref) => ( +
+
+

{pref.description}

+

{pref.notificationType}

+
+ handleToggle(pref.notificationType, value)} + isDisabled={isSaving} + size="sm" + /> +
+ ))} - {successMessage && ( -
-

{successMessage}

-
- )} + {preferences.length === 0 && !errorMessage && ( +
+

暂无可用的通知偏好设置

+
+ )} +
- {preferences.length === 0 && !errorMessage && ( -
-

暂无可用的通知偏好设置

-
- )} -
-
+ {/* Messages */} + {errorMessage && ( +
+

{errorMessage}

+
+ )} + + {successMessage && ( +
+

{successMessage}

+
+ )} + + )} + + + + + + + ) } diff --git a/src/pages/repair/RepairAdmin.tsx b/src/pages/repair/RepairAdmin.tsx index 1d50ff5..eb70090 100644 --- a/src/pages/repair/RepairAdmin.tsx +++ b/src/pages/repair/RepairAdmin.tsx @@ -526,20 +526,13 @@ export default function App() {
维修管理
- { - userInfo?.roles?.find(v => v.toLowerCase() == "repair admin") - ?
- : <> - } -
- - {/* Notification Preferences Section */} - {token && ( -
- +
+ {token && } + {userInfo?.roles?.find(v => v.toLowerCase() == "repair admin") && ( + + )}
- )} - +
{/* Mobile Cards Layout */}