create-fullstack-scaffold 0.4.9 → 0.4.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +499 -100
- package/dist/cli/index.js.map +1 -1
- package/package.json +2 -2
- package/template/CLAUDE.md +18 -6
- package/template/drizzle/0004_add_merchants_products.sql +30 -0
- package/template/drizzle/meta/_journal.json +7 -0
- package/template/eslint-rules/module-boundary.js +6 -2
- package/template/eslint-rules/no-cross-module-service-import.js +105 -0
- package/template/eslint-rules/no-disable-type-safe-client.js +5 -0
- package/template/eslint.config.js +9 -0
- package/template/lint-scripts/config/project.config.ts +21 -0
- package/template/lint-scripts/validate-all.ts +48 -12
- package/template/lint-scripts/validators/index.ts +29 -0
- package/template/lint-scripts/validators/module-public-api.validator.ts +103 -0
- package/template/lint-scripts/validators/schema-uniqueness.validator.ts +147 -0
- package/template/modules.config.ts +2 -0
- package/template/package.json +2 -0
- package/template/playwright.config.ts +14 -0
- package/template/src/admin/App.tsx +40 -3
- package/template/src/admin/components/LanguageSwitcher.tsx +22 -0
- package/template/src/admin/components/NotificationDrawer.tsx +108 -38
- package/template/src/admin/components/StatsCard.tsx +3 -1
- package/template/src/admin/components/ThemeToggle.tsx +28 -0
- package/template/src/admin/hooks/useRoles.ts +2 -2
- package/template/src/admin/i18n/index.ts +18 -0
- package/template/src/admin/i18n/locales/en-US.json +381 -0
- package/template/src/admin/i18n/locales/zh-CN.json +381 -0
- package/template/src/admin/i18n/useLanguage.ts +21 -0
- package/template/src/admin/layouts/Header.tsx +37 -16
- package/template/src/admin/layouts/Layout.tsx +7 -3
- package/template/src/admin/layouts/Sidebar.tsx +70 -68
- package/template/src/admin/main.tsx +1 -0
- package/template/src/admin/pages/ContentPage.tsx +68 -56
- package/template/src/admin/pages/DashboardPage.tsx +82 -76
- package/template/src/admin/pages/DisputesPage.tsx +61 -53
- package/template/src/admin/pages/LoginPage.tsx +41 -33
- package/template/src/admin/pages/OrdersPage.tsx +69 -64
- package/template/src/admin/pages/PermissionsPage.tsx +10 -8
- package/template/src/admin/pages/PluginDashboardPage.tsx +3 -1
- package/template/src/admin/pages/PluginManagementPage.tsx +3 -1
- package/template/src/admin/pages/RegisterPage.tsx +18 -16
- package/template/src/admin/pages/RolesPage.tsx +51 -49
- package/template/src/admin/pages/SettingsPage.tsx +22 -22
- package/template/src/admin/pages/SystemLogsPage.tsx +32 -30
- package/template/src/admin/pages/TicketsPage.tsx +67 -59
- package/template/src/admin/pages/UsersPage.tsx +63 -53
- package/template/src/admin/pages/__tests__/RolesPage.test.tsx +2 -2
- package/template/src/admin/stores/themeStore.ts +32 -0
- package/template/src/cli/modules/admin/index.ts +200 -0
- package/template/src/cli/modules/captcha/index.ts +40 -0
- package/template/src/cli/modules/chat/index.ts +21 -0
- package/template/src/cli/modules/content/index.ts +144 -0
- package/template/src/cli/modules/dispute/index.ts +150 -0
- package/template/src/cli/modules/file/index.ts +55 -0
- package/template/src/cli/modules/index.ts +30 -5
- package/template/src/cli/modules/order/index.ts +170 -0
- package/template/src/cli/modules/permission/index.ts +195 -0
- package/template/src/cli/modules/tenant/index.ts +142 -0
- package/template/src/cli/modules/ticket/index.ts +167 -0
- package/template/src/cli/rpc/client.ts +2 -2
- package/template/src/client/index.css +73 -0
- package/template/src/merchant/App.tsx +2 -0
- package/template/src/merchant/components/MerchantGuard.tsx +2 -6
- package/template/src/merchant/components/__tests__/MerchantGuard.test.tsx +117 -0
- package/template/src/merchant/layouts/Header.tsx +2 -2
- package/template/src/merchant/pages/DashboardPage.tsx +2 -2
- package/template/src/merchant/pages/DisputesPage.tsx +14 -10
- package/template/src/merchant/pages/LoginPage.tsx +49 -0
- package/template/src/merchant/pages/OrdersPage.tsx +17 -10
- package/template/src/merchant/pages/ProductsPage.tsx +31 -8
- package/template/src/merchant/pages/SettingsPage.tsx +3 -7
- package/template/src/merchant/stores/__tests__/merchantStore.test.ts +317 -0
- package/template/src/merchant/stores/merchantStore.ts +24 -27
- package/template/src/server/db/schema/index.ts +2 -0
- package/template/src/server/db/schema/merchants.ts +26 -0
- package/template/src/server/db/schema/products.ts +25 -0
- package/template/src/server/db/test-setup.ts +237 -0
- package/template/src/server/middleware/__tests__/audit-log.test.ts +144 -0
- package/template/src/server/middleware/__tests__/cors.test.ts +84 -0
- package/template/src/server/middleware/__tests__/logger.test.ts +117 -0
- package/template/src/server/middleware/__tests__/rate-limit.test.ts +73 -0
- package/template/src/server/middleware/__tests__/realtime-env.test.ts +56 -0
- package/template/src/server/middleware/__tests__/tenant-isolation.test.ts +150 -0
- package/template/src/server/module-admin/__tests__/admin-routes.test.ts +1 -1
- package/template/src/server/module-admin/module.ts +4 -0
- package/template/src/server/module-admin/routes/admin-notification-routes.ts +3 -3
- package/template/src/server/module-admin/routes/user-management-routes.ts +2 -2
- package/template/src/server/module-auth/__tests__/auth-routes.test.ts +315 -0
- package/template/src/server/module-auth/__tests__/profile-routes.test.ts +83 -0
- package/template/src/server/module-auth/module.ts +2 -0
- package/template/src/server/module-content/module.ts +1 -0
- package/template/src/server/module-content/routes/content-routes.ts +2 -2
- package/template/src/server/module-dispute/module.ts +1 -0
- package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -2
- package/template/src/server/module-merchant/__tests__/merchant-routes.test.ts +218 -0
- package/template/src/server/module-merchant/__tests__/merchant-service.test.ts +183 -0
- package/template/src/server/module-merchant/index.ts +10 -0
- package/template/src/server/module-merchant/module.ts +33 -0
- package/template/src/server/module-merchant/routes/merchant-routes.ts +138 -0
- package/template/src/server/module-merchant/services/merchant-service.ts +292 -0
- package/template/src/server/module-notifications/index.ts +18 -0
- package/template/src/server/module-notifications/module.ts +2 -0
- package/template/src/server/module-order/module.ts +1 -0
- package/template/src/server/module-order/routes/order-routes.ts +2 -2
- package/template/src/server/module-permission/routes/role-routes.ts +3 -3
- package/template/src/server/module-plugin/__tests__/admin-category-service.test.ts +181 -0
- package/template/src/server/module-plugin/__tests__/admin-plugin-service.test.ts +277 -0
- package/template/src/server/module-plugin/__tests__/admin-stats-service.test.ts +170 -0
- package/template/src/server/module-plugin/__tests__/plugin-review-service.test.ts +260 -0
- package/template/src/server/module-plugin/__tests__/plugin-seed-service.test.ts +170 -0
- package/template/src/server/module-plugin/module.ts +3 -0
- package/template/src/server/module-tenant/__tests__/tenant-routes.test.ts +142 -0
- package/template/src/server/module-tenant/__tests__/tenant-service.test.ts +152 -0
- package/template/src/server/module-tenant/module.ts +1 -0
- package/template/src/server/module-tenant/services/tenant-service.ts +1 -37
- package/template/src/server/module-ticket/module.ts +1 -0
- package/template/src/server/module-ticket/routes/ticket-routes.ts +2 -2
- package/template/src/server/module-todos/module.ts +3 -0
- package/template/src/server/route-registry.ts +4 -0
- package/template/src/server/utils/__tests__/date.test.ts +130 -0
- package/template/src/server/utils/__tests__/env.test.ts +25 -0
- package/template/src/server/utils/__tests__/generate.test.ts +82 -0
- package/template/src/server/utils/__tests__/id-helpers.test.ts +29 -0
- package/template/src/server/utils/__tests__/json.test.ts +49 -0
- package/template/src/server/utils/__tests__/permission-utils.test.ts +26 -0
- package/template/src/server/utils/__tests__/uuid.test.ts +25 -0
- package/template/src/shared/core/module-manifest.ts +15 -0
- package/template/src/shared/modules/admin/schemas.ts +1 -1
- package/template/src/shared/modules/content/schemas.ts +2 -2
- package/template/src/shared/modules/dispute/schemas.ts +2 -2
- package/template/src/shared/modules/index.ts +181 -1
- package/template/src/shared/modules/merchant/index.ts +12 -0
- package/template/src/shared/modules/merchant/schemas.ts +63 -0
- package/template/src/shared/modules/order/schemas.ts +2 -2
- package/template/src/shared/modules/permission/index.ts +1 -1
- package/template/src/shared/modules/permission/schemas.ts +1 -1
- package/template/src/shared/modules/role/schemas.ts +2 -2
- package/template/src/shared/modules/ticket/schemas.ts +2 -2
- package/template/src/shared/schemas/index.ts +74 -2
|
@@ -5,15 +5,16 @@ import { PermissionGuard } from '../components/PermissionGuard'
|
|
|
5
5
|
import { Permission } from '@shared/modules/permission'
|
|
6
6
|
import { apiClient } from '../services/apiClient'
|
|
7
7
|
import type { Ticket } from '@shared/modules/ticket'
|
|
8
|
+
import { useLanguage } from '../i18n/useLanguage'
|
|
8
9
|
|
|
9
|
-
const PRIORITY_COLORS = {
|
|
10
|
+
const PRIORITY_COLORS: Record<string, string> = {
|
|
10
11
|
low: 'default',
|
|
11
12
|
medium: 'blue',
|
|
12
13
|
high: 'orange',
|
|
13
14
|
urgent: 'red',
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
const STATUS_COLORS = {
|
|
17
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
17
18
|
open: 'blue',
|
|
18
19
|
in_progress: 'processing',
|
|
19
20
|
waiting_customer: 'orange',
|
|
@@ -21,27 +22,34 @@ const STATUS_COLORS = {
|
|
|
21
22
|
closed: 'default',
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
const PRIORITY_LABELS = {
|
|
25
|
-
low: '低',
|
|
26
|
-
medium: '中',
|
|
27
|
-
high: '高',
|
|
28
|
-
urgent: '紧急',
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const STATUS_LABELS = {
|
|
32
|
-
open: '待处理',
|
|
33
|
-
in_progress: '处理中',
|
|
34
|
-
waiting_customer: '等待客户',
|
|
35
|
-
resolved: '已解决',
|
|
36
|
-
closed: '已关闭',
|
|
37
|
-
}
|
|
38
|
-
|
|
39
25
|
export const TicketsPage: React.FC = () => {
|
|
26
|
+
const { t, formatDate } = useLanguage()
|
|
40
27
|
const [tickets, setTickets] = useState<Ticket[]>([])
|
|
41
28
|
const [loading, setLoading] = useState(false)
|
|
42
29
|
const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null)
|
|
43
30
|
const [detailVisible, setDetailVisible] = useState(false)
|
|
44
31
|
|
|
32
|
+
const getPriorityLabel = (priority: string) => {
|
|
33
|
+
const map: Record<string, string> = {
|
|
34
|
+
low: t('tickets.priorityLow'),
|
|
35
|
+
medium: t('tickets.priorityMedium'),
|
|
36
|
+
high: t('tickets.priorityHigh'),
|
|
37
|
+
urgent: t('tickets.priorityUrgent'),
|
|
38
|
+
}
|
|
39
|
+
return map[priority] || priority
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const getStatusLabel = (status: string) => {
|
|
43
|
+
const map: Record<string, string> = {
|
|
44
|
+
open: t('tickets.statusOpen'),
|
|
45
|
+
in_progress: t('tickets.statusInProgress'),
|
|
46
|
+
waiting_customer: t('tickets.statusWaitingCustomer'),
|
|
47
|
+
resolved: t('tickets.statusResolved'),
|
|
48
|
+
closed: t('tickets.statusClosed'),
|
|
49
|
+
}
|
|
50
|
+
return map[status] || status
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
useEffect(() => {
|
|
46
54
|
fetchTickets()
|
|
47
55
|
}, [])
|
|
@@ -54,10 +62,10 @@ export const TicketsPage: React.FC = () => {
|
|
|
54
62
|
if (result.success) {
|
|
55
63
|
setTickets(result.data)
|
|
56
64
|
} else {
|
|
57
|
-
message.error(result.error || '
|
|
65
|
+
message.error(result.error || t('tickets.loadFailed'))
|
|
58
66
|
}
|
|
59
67
|
} catch {
|
|
60
|
-
message.error('
|
|
68
|
+
message.error(t('tickets.loadFailed'))
|
|
61
69
|
} finally {
|
|
62
70
|
setLoading(false)
|
|
63
71
|
}
|
|
@@ -65,8 +73,8 @@ export const TicketsPage: React.FC = () => {
|
|
|
65
73
|
|
|
66
74
|
const handleClose = async (ticketId: string) => {
|
|
67
75
|
Modal.confirm({
|
|
68
|
-
title: '
|
|
69
|
-
content: '
|
|
76
|
+
title: t('tickets.confirmClose'),
|
|
77
|
+
content: t('tickets.confirmCloseContent'),
|
|
70
78
|
onOk: async () => {
|
|
71
79
|
try {
|
|
72
80
|
const response = await apiClient.api.tickets[':id'].close.$put({
|
|
@@ -74,11 +82,11 @@ export const TicketsPage: React.FC = () => {
|
|
|
74
82
|
})
|
|
75
83
|
const result = await response.json()
|
|
76
84
|
if (result.success) {
|
|
77
|
-
message.success('
|
|
85
|
+
message.success(t('tickets.ticketClosed'))
|
|
78
86
|
fetchTickets()
|
|
79
87
|
}
|
|
80
88
|
} catch {
|
|
81
|
-
message.error('
|
|
89
|
+
message.error(t('tickets.closeFailed'))
|
|
82
90
|
}
|
|
83
91
|
},
|
|
84
92
|
})
|
|
@@ -91,18 +99,18 @@ export const TicketsPage: React.FC = () => {
|
|
|
91
99
|
|
|
92
100
|
const columns = [
|
|
93
101
|
{
|
|
94
|
-
title: '
|
|
102
|
+
title: t('tickets.ticketNo'),
|
|
95
103
|
dataIndex: 'ticketNo',
|
|
96
104
|
key: 'ticketNo',
|
|
97
105
|
render: (text: string) => <code className="text-sm">{text}</code>,
|
|
98
106
|
},
|
|
99
107
|
{
|
|
100
|
-
title: '
|
|
108
|
+
title: t('tickets.subject'),
|
|
101
109
|
dataIndex: 'subject',
|
|
102
110
|
key: 'subject',
|
|
103
111
|
},
|
|
104
112
|
{
|
|
105
|
-
title: '
|
|
113
|
+
title: t('tickets.customer'),
|
|
106
114
|
key: 'customer',
|
|
107
115
|
render: (_: unknown, record: Ticket) => (
|
|
108
116
|
<div>
|
|
@@ -112,29 +120,29 @@ export const TicketsPage: React.FC = () => {
|
|
|
112
120
|
),
|
|
113
121
|
},
|
|
114
122
|
{
|
|
115
|
-
title: '
|
|
123
|
+
title: t('tickets.priority'),
|
|
116
124
|
dataIndex: 'priority',
|
|
117
125
|
key: 'priority',
|
|
118
|
-
render: (priority:
|
|
119
|
-
<Tag color={PRIORITY_COLORS[priority]}>{
|
|
126
|
+
render: (priority: string) => (
|
|
127
|
+
<Tag color={PRIORITY_COLORS[priority]}>{getPriorityLabel(priority)}</Tag>
|
|
120
128
|
),
|
|
121
129
|
},
|
|
122
130
|
{
|
|
123
|
-
title: '
|
|
131
|
+
title: t('common.status'),
|
|
124
132
|
dataIndex: 'status',
|
|
125
133
|
key: 'status',
|
|
126
|
-
render: (status:
|
|
127
|
-
<Tag color={STATUS_COLORS[status]}>{
|
|
134
|
+
render: (status: string) => (
|
|
135
|
+
<Tag color={STATUS_COLORS[status]}>{getStatusLabel(status)}</Tag>
|
|
128
136
|
),
|
|
129
137
|
},
|
|
130
138
|
{
|
|
131
|
-
title: '
|
|
139
|
+
title: t('common.createdAt'),
|
|
132
140
|
dataIndex: 'createdAt',
|
|
133
141
|
key: 'createdAt',
|
|
134
|
-
render: (date: string) =>
|
|
142
|
+
render: (date: string) => formatDate(date),
|
|
135
143
|
},
|
|
136
144
|
{
|
|
137
|
-
title: '
|
|
145
|
+
title: t('common.actions'),
|
|
138
146
|
key: 'actions',
|
|
139
147
|
render: (_: unknown, record: Ticket) => (
|
|
140
148
|
<Space>
|
|
@@ -144,7 +152,7 @@ export const TicketsPage: React.FC = () => {
|
|
|
144
152
|
icon={<Eye className="w-4 h-4" />}
|
|
145
153
|
onClick={() => showDetail(record)}
|
|
146
154
|
>
|
|
147
|
-
|
|
155
|
+
{t('common.view')}
|
|
148
156
|
</Button>
|
|
149
157
|
<PermissionGuard permission={Permission.TICKET_CLOSE}>
|
|
150
158
|
{record.status !== 'closed' && (
|
|
@@ -154,7 +162,7 @@ export const TicketsPage: React.FC = () => {
|
|
|
154
162
|
icon={<CheckCircle className="w-4 h-4" />}
|
|
155
163
|
onClick={() => handleClose(record.id)}
|
|
156
164
|
>
|
|
157
|
-
|
|
165
|
+
{t('common.close')}
|
|
158
166
|
</Button>
|
|
159
167
|
)}
|
|
160
168
|
</PermissionGuard>
|
|
@@ -167,17 +175,17 @@ export const TicketsPage: React.FC = () => {
|
|
|
167
175
|
<div>
|
|
168
176
|
<div className="flex justify-between items-center mb-6">
|
|
169
177
|
<div>
|
|
170
|
-
<h1 className="text-2xl font-bold text-gray-900"
|
|
171
|
-
<p className="text-gray-600 mt-1"
|
|
178
|
+
<h1 className="text-2xl font-bold text-gray-900">{t('tickets.title')}</h1>
|
|
179
|
+
<p className="text-gray-600 mt-1">{t('tickets.subtitle')}</p>
|
|
172
180
|
</div>
|
|
173
|
-
<Button onClick={fetchTickets}
|
|
181
|
+
<Button onClick={fetchTickets}>{t('common.refresh')}</Button>
|
|
174
182
|
</div>
|
|
175
183
|
|
|
176
184
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
|
177
185
|
<Card>
|
|
178
186
|
<div className="flex items-center justify-between">
|
|
179
187
|
<div>
|
|
180
|
-
<div className="text-sm text-gray-500"
|
|
188
|
+
<div className="text-sm text-gray-500">{t('tickets.totalTickets')}</div>
|
|
181
189
|
<div className="text-2xl font-bold">{tickets.length}</div>
|
|
182
190
|
</div>
|
|
183
191
|
<MessageCircle className="w-8 h-8 text-blue-500" />
|
|
@@ -186,7 +194,7 @@ export const TicketsPage: React.FC = () => {
|
|
|
186
194
|
<Card>
|
|
187
195
|
<div className="flex items-center justify-between">
|
|
188
196
|
<div>
|
|
189
|
-
<div className="text-sm text-gray-500"
|
|
197
|
+
<div className="text-sm text-gray-500">{t('tickets.pending')}</div>
|
|
190
198
|
<div className="text-2xl font-bold text-blue-500">
|
|
191
199
|
{tickets.filter(t => t.status === 'open').length}
|
|
192
200
|
</div>
|
|
@@ -197,7 +205,7 @@ export const TicketsPage: React.FC = () => {
|
|
|
197
205
|
<Card>
|
|
198
206
|
<div className="flex items-center justify-between">
|
|
199
207
|
<div>
|
|
200
|
-
<div className="text-sm text-gray-500"
|
|
208
|
+
<div className="text-sm text-gray-500">{t('tickets.urgent')}</div>
|
|
201
209
|
<div className="text-2xl font-bold text-red-500">
|
|
202
210
|
{tickets.filter(t => t.priority === 'urgent').length}
|
|
203
211
|
</div>
|
|
@@ -208,7 +216,7 @@ export const TicketsPage: React.FC = () => {
|
|
|
208
216
|
<Card>
|
|
209
217
|
<div className="flex items-center justify-between">
|
|
210
218
|
<div>
|
|
211
|
-
<div className="text-sm text-gray-500"
|
|
219
|
+
<div className="text-sm text-gray-500">{t('tickets.resolved')}</div>
|
|
212
220
|
<div className="text-2xl font-bold text-green-500">
|
|
213
221
|
{tickets.filter(t => t.status === 'resolved' || t.status === 'closed').length}
|
|
214
222
|
</div>
|
|
@@ -226,13 +234,13 @@ export const TicketsPage: React.FC = () => {
|
|
|
226
234
|
loading={loading}
|
|
227
235
|
pagination={{
|
|
228
236
|
pageSize: 10,
|
|
229
|
-
showTotal: total =>
|
|
237
|
+
showTotal: total => t('common.total', { count: total }),
|
|
230
238
|
}}
|
|
231
239
|
/>
|
|
232
240
|
</Card>
|
|
233
241
|
|
|
234
242
|
<Modal
|
|
235
|
-
title=
|
|
243
|
+
title={t('tickets.detail')}
|
|
236
244
|
open={detailVisible}
|
|
237
245
|
onCancel={() => setDetailVisible(false)}
|
|
238
246
|
footer={null}
|
|
@@ -241,35 +249,35 @@ export const TicketsPage: React.FC = () => {
|
|
|
241
249
|
{selectedTicket && (
|
|
242
250
|
<div>
|
|
243
251
|
<Descriptions column={2} bordered className="mb-4">
|
|
244
|
-
<Descriptions.Item label=
|
|
252
|
+
<Descriptions.Item label={t('tickets.ticketNo')}>
|
|
245
253
|
<code>{selectedTicket.ticketNo}</code>
|
|
246
254
|
</Descriptions.Item>
|
|
247
|
-
<Descriptions.Item label=
|
|
255
|
+
<Descriptions.Item label={t('common.status')}>
|
|
248
256
|
<Tag color={STATUS_COLORS[selectedTicket.status]}>
|
|
249
|
-
{
|
|
257
|
+
{getStatusLabel(selectedTicket.status)}
|
|
250
258
|
</Tag>
|
|
251
259
|
</Descriptions.Item>
|
|
252
|
-
<Descriptions.Item label=
|
|
253
|
-
<Descriptions.Item label=
|
|
254
|
-
<Descriptions.Item label=
|
|
260
|
+
<Descriptions.Item label={t('tickets.customerName')}>{selectedTicket.customerName}</Descriptions.Item>
|
|
261
|
+
<Descriptions.Item label={t('tickets.customerEmail')}>{selectedTicket.customerEmail}</Descriptions.Item>
|
|
262
|
+
<Descriptions.Item label={t('tickets.priority')}>
|
|
255
263
|
<Tag color={PRIORITY_COLORS[selectedTicket.priority]}>
|
|
256
|
-
{
|
|
264
|
+
{getPriorityLabel(selectedTicket.priority)}
|
|
257
265
|
</Tag>
|
|
258
266
|
</Descriptions.Item>
|
|
259
|
-
<Descriptions.Item label=
|
|
260
|
-
{selectedTicket.assignedTo || '
|
|
267
|
+
<Descriptions.Item label={t('tickets.assignedTo')}>
|
|
268
|
+
{selectedTicket.assignedTo || t('tickets.unassigned')}
|
|
261
269
|
</Descriptions.Item>
|
|
262
|
-
<Descriptions.Item label=
|
|
270
|
+
<Descriptions.Item label={t('tickets.subject')} span={2}>
|
|
263
271
|
{selectedTicket.subject}
|
|
264
272
|
</Descriptions.Item>
|
|
265
|
-
<Descriptions.Item label=
|
|
273
|
+
<Descriptions.Item label={t('tickets.description')} span={2}>
|
|
266
274
|
{selectedTicket.description}
|
|
267
275
|
</Descriptions.Item>
|
|
268
276
|
</Descriptions>
|
|
269
277
|
|
|
270
278
|
{selectedTicket.replies.length > 0 && (
|
|
271
279
|
<div className="mt-4">
|
|
272
|
-
<h4 className="font-semibold mb-2"
|
|
280
|
+
<h4 className="font-semibold mb-2">{t('tickets.replies')}</h4>
|
|
273
281
|
{selectedTicket.replies.map(reply => (
|
|
274
282
|
<div
|
|
275
283
|
key={reply.id}
|
|
@@ -278,7 +286,7 @@ export const TicketsPage: React.FC = () => {
|
|
|
278
286
|
<div className="flex justify-between items-center mb-1">
|
|
279
287
|
<span className="font-medium">{reply.author}</span>
|
|
280
288
|
<span className="text-sm text-gray-500">
|
|
281
|
-
{
|
|
289
|
+
{formatDate(reply.createdAt)}
|
|
282
290
|
</span>
|
|
283
291
|
</div>
|
|
284
292
|
<div className="text-gray-700">{reply.content}</div>
|
|
@@ -25,10 +25,12 @@ import { useRoleLabels } from '../hooks/useConfig'
|
|
|
25
25
|
import { Permission, Role } from '@shared/modules/permission'
|
|
26
26
|
import { PermissionGuard } from '../components/PermissionGuard'
|
|
27
27
|
import type { User, CreateUserRequest } from '@shared/modules/admin'
|
|
28
|
+
import { useLanguage } from '../i18n/useLanguage'
|
|
28
29
|
|
|
29
30
|
type UserFormData = CreateUserRequest & { password?: string }
|
|
30
31
|
|
|
31
32
|
export const UsersPage: React.FC = () => {
|
|
33
|
+
const { t, formatDate } = useLanguage()
|
|
32
34
|
const [users, setUsers] = useState<User[]>([])
|
|
33
35
|
const [modalVisible, setModalVisible] = useState(false)
|
|
34
36
|
const [editingUser, setEditingUser] = useState<User | null>(null)
|
|
@@ -37,10 +39,10 @@ export const UsersPage: React.FC = () => {
|
|
|
37
39
|
|
|
38
40
|
const fetchUsers = async () => {
|
|
39
41
|
try {
|
|
40
|
-
const data = await api(apiClient.api.admin.users.$get()).withLoading('
|
|
42
|
+
const data = await api(apiClient.api.admin.users.$get()).withLoading(t('users.loading')).json()
|
|
41
43
|
setUsers(data)
|
|
42
44
|
} catch {
|
|
43
|
-
//
|
|
45
|
+
// handled by api-request
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -74,9 +76,9 @@ export const UsersPage: React.FC = () => {
|
|
|
74
76
|
json: values,
|
|
75
77
|
})
|
|
76
78
|
)
|
|
77
|
-
.withLoading('
|
|
79
|
+
.withLoading(t('users.updating'))
|
|
78
80
|
.json()
|
|
79
|
-
message.success('
|
|
81
|
+
message.success(t('users.updated'))
|
|
80
82
|
setModalVisible(false)
|
|
81
83
|
fetchUsers()
|
|
82
84
|
} else {
|
|
@@ -91,14 +93,14 @@ export const UsersPage: React.FC = () => {
|
|
|
91
93
|
},
|
|
92
94
|
})
|
|
93
95
|
)
|
|
94
|
-
.withLoading('
|
|
96
|
+
.withLoading(t('users.creating'))
|
|
95
97
|
.json()
|
|
96
|
-
message.success('
|
|
98
|
+
message.success(t('users.created'))
|
|
97
99
|
setModalVisible(false)
|
|
98
100
|
fetchUsers()
|
|
99
101
|
}
|
|
100
102
|
} catch {
|
|
101
|
-
//
|
|
103
|
+
// handled by api-request
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
106
|
|
|
@@ -113,10 +115,10 @@ export const UsersPage: React.FC = () => {
|
|
|
113
115
|
)
|
|
114
116
|
.withLoading()
|
|
115
117
|
.json()
|
|
116
|
-
message.success(newStatus === 'locked' ? '
|
|
118
|
+
message.success(newStatus === 'locked' ? t('users.userLocked') : t('users.userUnlocked'))
|
|
117
119
|
fetchUsers()
|
|
118
120
|
} catch {
|
|
119
|
-
//
|
|
121
|
+
// handled by api-request
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
|
|
@@ -127,63 +129,63 @@ export const UsersPage: React.FC = () => {
|
|
|
127
129
|
param: { id: userId },
|
|
128
130
|
})
|
|
129
131
|
)
|
|
130
|
-
.withLoading('
|
|
132
|
+
.withLoading(t('users.deleting'))
|
|
131
133
|
.json()
|
|
132
|
-
message.success('
|
|
134
|
+
message.success(t('users.deleted'))
|
|
133
135
|
fetchUsers()
|
|
134
136
|
} catch {
|
|
135
|
-
//
|
|
137
|
+
// handled by api-request
|
|
136
138
|
}
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
const getStatusTag = (status: string) => {
|
|
140
|
-
const statusConfig: Record<string, { color: string;
|
|
141
|
-
active: { color: 'green',
|
|
142
|
-
inactive: { color: 'orange',
|
|
143
|
-
locked: { color: 'red',
|
|
142
|
+
const statusConfig: Record<string, { color: string; textKey: string }> = {
|
|
143
|
+
active: { color: 'green', textKey: 'users.active' },
|
|
144
|
+
inactive: { color: 'orange', textKey: 'users.inactive' },
|
|
145
|
+
locked: { color: 'red', textKey: 'users.locked' },
|
|
144
146
|
}
|
|
145
147
|
const config = statusConfig[status]
|
|
146
148
|
if (!config) return <Tag>{status}</Tag>
|
|
147
|
-
return <Tag color={config.color}>{config.
|
|
149
|
+
return <Tag color={config.color}>{t(config.textKey)}</Tag>
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
const columns: ColumnsType<User> = [
|
|
151
153
|
{
|
|
152
|
-
title: '
|
|
154
|
+
title: t('users.username'),
|
|
153
155
|
dataIndex: 'username',
|
|
154
156
|
key: 'username',
|
|
155
157
|
},
|
|
156
158
|
{
|
|
157
|
-
title: '
|
|
159
|
+
title: t('users.email'),
|
|
158
160
|
dataIndex: 'email',
|
|
159
161
|
key: 'email',
|
|
160
162
|
},
|
|
161
163
|
{
|
|
162
|
-
title: '
|
|
164
|
+
title: t('users.role'),
|
|
163
165
|
dataIndex: 'role',
|
|
164
166
|
key: 'role',
|
|
165
167
|
render: (role: Role) => <Tag color="blue">{roleLabels[role] || role}</Tag>,
|
|
166
168
|
},
|
|
167
169
|
{
|
|
168
|
-
title: '
|
|
170
|
+
title: t('users.status'),
|
|
169
171
|
dataIndex: 'status',
|
|
170
172
|
key: 'status',
|
|
171
173
|
render: (status: string) => getStatusTag(status),
|
|
172
174
|
},
|
|
173
175
|
{
|
|
174
|
-
title: '
|
|
176
|
+
title: t('common.createdAt'),
|
|
175
177
|
dataIndex: 'createdAt',
|
|
176
178
|
key: 'createdAt',
|
|
177
|
-
render: (date: string) =>
|
|
179
|
+
render: (date: string) => formatDate(date),
|
|
178
180
|
},
|
|
179
181
|
{
|
|
180
|
-
title: '
|
|
182
|
+
title: t('common.actions'),
|
|
181
183
|
key: 'actions',
|
|
182
184
|
render: (_: unknown, record: User) => (
|
|
183
185
|
<Space>
|
|
184
186
|
<PermissionGuard permission={Permission.USER_EDIT}>
|
|
185
187
|
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>
|
|
186
|
-
|
|
188
|
+
{t('common.edit')}
|
|
187
189
|
</Button>
|
|
188
190
|
</PermissionGuard>
|
|
189
191
|
<PermissionGuard permission={Permission.USER_EDIT}>
|
|
@@ -193,18 +195,18 @@ export const UsersPage: React.FC = () => {
|
|
|
193
195
|
onClick={() => handleToggleLock(record)}
|
|
194
196
|
danger={record.status !== 'locked'}
|
|
195
197
|
>
|
|
196
|
-
{record.status === 'locked' ? '
|
|
198
|
+
{record.status === 'locked' ? t('users.unlock') : t('users.lock')}
|
|
197
199
|
</Button>
|
|
198
200
|
</PermissionGuard>
|
|
199
201
|
<PermissionGuard permission={Permission.USER_DELETE}>
|
|
200
202
|
<Popconfirm
|
|
201
|
-
title=
|
|
203
|
+
title={t('users.deleteConfirm')}
|
|
202
204
|
onConfirm={() => handleDelete(record.id)}
|
|
203
|
-
okText=
|
|
204
|
-
cancelText=
|
|
205
|
+
okText={t('common.confirm')}
|
|
206
|
+
cancelText={t('common.cancel')}
|
|
205
207
|
>
|
|
206
208
|
<Button type="link" icon={<DeleteOutlined />} danger>
|
|
207
|
-
|
|
209
|
+
{t('common.delete')}
|
|
208
210
|
</Button>
|
|
209
211
|
</Popconfirm>
|
|
210
212
|
</PermissionGuard>
|
|
@@ -216,11 +218,11 @@ export const UsersPage: React.FC = () => {
|
|
|
216
218
|
return (
|
|
217
219
|
<div className="p-6">
|
|
218
220
|
<Card
|
|
219
|
-
title=
|
|
221
|
+
title={t('users.title')}
|
|
220
222
|
extra={
|
|
221
223
|
<PermissionGuard permission={Permission.USER_CREATE}>
|
|
222
224
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
|
|
223
|
-
|
|
225
|
+
{t('users.createUser')}
|
|
224
226
|
</Button>
|
|
225
227
|
</PermissionGuard>
|
|
226
228
|
}
|
|
@@ -229,7 +231,7 @@ export const UsersPage: React.FC = () => {
|
|
|
229
231
|
</Card>
|
|
230
232
|
|
|
231
233
|
<Modal
|
|
232
|
-
title={editingUser ? '
|
|
234
|
+
title={editingUser ? t('users.editUser') : t('users.createUser')}
|
|
233
235
|
open={modalVisible}
|
|
234
236
|
onCancel={() => setModalVisible(false)}
|
|
235
237
|
onOk={() => form.submit()}
|
|
@@ -238,42 +240,50 @@ export const UsersPage: React.FC = () => {
|
|
|
238
240
|
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
|
239
241
|
<Form.Item
|
|
240
242
|
name="username"
|
|
241
|
-
label=
|
|
242
|
-
rules={[{ required: true, message: '
|
|
243
|
+
label={t('users.username')}
|
|
244
|
+
rules={[{ required: true, message: t('users.usernameRequired') }]}
|
|
243
245
|
>
|
|
244
|
-
<Input placeholder=
|
|
246
|
+
<Input placeholder={t('users.usernamePlaceholder')} />
|
|
245
247
|
</Form.Item>
|
|
246
248
|
<Form.Item
|
|
247
249
|
name="email"
|
|
248
|
-
label=
|
|
250
|
+
label={t('users.email')}
|
|
249
251
|
rules={[
|
|
250
|
-
{ required: true, message: '
|
|
251
|
-
{ type: 'email', message: '
|
|
252
|
+
{ required: true, message: t('users.emailRequired') },
|
|
253
|
+
{ type: 'email', message: t('users.emailInvalid') },
|
|
252
254
|
]}
|
|
253
255
|
>
|
|
254
|
-
<Input placeholder=
|
|
256
|
+
<Input placeholder={t('users.emailPlaceholder')} />
|
|
255
257
|
</Form.Item>
|
|
256
258
|
{!editingUser && (
|
|
257
259
|
<Form.Item
|
|
258
260
|
name="password"
|
|
259
|
-
label=
|
|
260
|
-
rules={[{ required: true, message: '
|
|
261
|
+
label={t('users.password')}
|
|
262
|
+
rules={[{ required: true, message: t('users.passwordRequired') }]}
|
|
261
263
|
>
|
|
262
|
-
<Input.Password placeholder=
|
|
264
|
+
<Input.Password placeholder={t('users.passwordPlaceholder')} />
|
|
263
265
|
</Form.Item>
|
|
264
266
|
)}
|
|
265
|
-
<Form.Item
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
267
|
+
<Form.Item
|
|
268
|
+
name="role"
|
|
269
|
+
label={t('users.role')}
|
|
270
|
+
rules={[{ required: true, message: t('users.roleRequired') }]}
|
|
271
|
+
>
|
|
272
|
+
<Select placeholder={t('users.rolePlaceholder')}>
|
|
273
|
+
<Select.Option value={Role.SUPER_ADMIN}>{t('users.superAdmin')}</Select.Option>
|
|
274
|
+
<Select.Option value={Role.CUSTOMER_SERVICE}>{t('users.customerService')}</Select.Option>
|
|
275
|
+
<Select.Option value={Role.USER}>{t('users.normalUser')}</Select.Option>
|
|
270
276
|
</Select>
|
|
271
277
|
</Form.Item>
|
|
272
|
-
<Form.Item
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
278
|
+
<Form.Item
|
|
279
|
+
name="status"
|
|
280
|
+
label={t('users.status')}
|
|
281
|
+
rules={[{ required: true, message: t('users.statusRequired') }]}
|
|
282
|
+
>
|
|
283
|
+
<Select placeholder={t('users.statusPlaceholder')}>
|
|
284
|
+
<Select.Option value="active">{t('users.active')}</Select.Option>
|
|
285
|
+
<Select.Option value="inactive">{t('users.inactive')}</Select.Option>
|
|
286
|
+
<Select.Option value="locked">{t('users.locked')}</Select.Option>
|
|
277
287
|
</Select>
|
|
278
288
|
</Form.Item>
|
|
279
289
|
</Form>
|
|
@@ -2,9 +2,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
2
2
|
import { render, screen, waitFor } from '@testing-library/react'
|
|
3
3
|
import userEvent from '@testing-library/user-event'
|
|
4
4
|
import { RolesPage } from '../RolesPage'
|
|
5
|
-
import type {
|
|
5
|
+
import type { RoleDataType } from '@shared/modules/role/schemas'
|
|
6
6
|
|
|
7
|
-
const mockRoles:
|
|
7
|
+
const mockRoles: RoleDataType[] = [
|
|
8
8
|
{
|
|
9
9
|
id: '1',
|
|
10
10
|
code: 'super_admin',
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
import { persist } from 'zustand/middleware'
|
|
3
|
+
|
|
4
|
+
type ThemeMode = 'light' | 'dark'
|
|
5
|
+
|
|
6
|
+
interface ThemeState {
|
|
7
|
+
mode: ThemeMode
|
|
8
|
+
toggleTheme: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const useThemeStore = create<ThemeState>()(
|
|
12
|
+
persist(
|
|
13
|
+
(set, get) => ({
|
|
14
|
+
mode: 'light' as ThemeMode,
|
|
15
|
+
toggleTheme: () => {
|
|
16
|
+
const newMode = get().mode === 'light' ? 'dark' : 'light'
|
|
17
|
+
document.documentElement.setAttribute('data-theme', newMode)
|
|
18
|
+
set({ mode: newMode })
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
{
|
|
22
|
+
name: 'admin-theme',
|
|
23
|
+
onRehydrateStorage: () => {
|
|
24
|
+
return (state) => {
|
|
25
|
+
if (state) {
|
|
26
|
+
document.documentElement.setAttribute('data-theme', state.mode)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
)
|