create-cundi-app 1.0.17 → 1.0.19

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/README.md CHANGED
@@ -39,6 +39,7 @@ The generated project includes:
39
39
  - 🛡️ **RBAC**: Role-based Access Control
40
40
  - 🌍 **i18n**: English and Traditional Chinese
41
41
  - 🌙 **Dark Mode**: Theme toggle support
42
+ - 🤖 **AI Assistant**: Persistent right-side chat panel (Optional)
42
43
  - 📊 **Dashboard**: Starter template
43
44
 
44
45
  ## After Creating
package/index.js CHANGED
@@ -52,6 +52,14 @@ async function main() {
52
52
  initial: false,
53
53
  });
54
54
 
55
+ // Ask if chat widget should be included
56
+ const { includeChatWidget } = await prompts({
57
+ type: "confirm",
58
+ name: "includeChatWidget",
59
+ message: "Include Chat Widget? (@cundi/react-chat-widget)",
60
+ initial: false,
61
+ });
62
+
55
63
  const targetDir = path.resolve(process.cwd(), projectName);
56
64
 
57
65
  // Check if directory already exists
@@ -76,6 +84,9 @@ async function main() {
76
84
  if (includeSamples) {
77
85
  console.log(kleur.gray("Including sample pages..."));
78
86
  }
87
+ if (includeChatWidget) {
88
+ console.log(kleur.gray("Including Chat Widget..."));
89
+ }
79
90
  console.log();
80
91
 
81
92
  // Copy template files
@@ -93,6 +104,13 @@ async function main() {
93
104
  if (!includeSamples && relativePath.includes("pages/samples")) {
94
105
  return false;
95
106
  }
107
+ // Skip chat components if not requested
108
+ if (!includeChatWidget && (
109
+ relativePath.includes("components/ChatPanel.tsx") ||
110
+ relativePath.includes("components/HeaderWithChat.tsx")
111
+ )) {
112
+ return false;
113
+ }
96
114
  // Skip the alternative version files (they will be handled separately)
97
115
  if (relativePath.endsWith(".no-samples.tsx") || relativePath.endsWith(".no-samples.ts")) {
98
116
  return false;
@@ -102,21 +120,55 @@ async function main() {
102
120
  });
103
121
 
104
122
  // Handle App.tsx and i18n.ts based on sample choice
123
+ const appPath = path.join(targetDir, "src/App.tsx");
124
+ const i18nPath = path.join(targetDir, "src/i18n.ts");
125
+
105
126
  if (!includeSamples) {
106
127
  // If not including samples, use the no-samples versions
107
128
  const appNoSamplesPath = path.join(templateDir, "src/App.no-samples.tsx");
108
- const appPath = path.join(targetDir, "src/App.tsx");
109
129
  if (fs.existsSync(appNoSamplesPath)) {
110
130
  await fs.copy(appNoSamplesPath, appPath, { overwrite: true });
111
131
  }
112
132
 
113
133
  const i18nNoSamplesPath = path.join(templateDir, "src/i18n.no-samples.ts");
114
- const i18nPath = path.join(targetDir, "src/i18n.ts");
115
134
  if (fs.existsSync(i18nNoSamplesPath)) {
116
135
  await fs.copy(i18nNoSamplesPath, i18nPath, { overwrite: true });
117
136
  }
118
137
  }
119
138
 
139
+ // Process chat markers in App.tsx
140
+ if (fs.existsSync(appPath)) {
141
+ let appContent = await fs.readFile(appPath, "utf-8");
142
+ if (includeChatWidget) {
143
+ // Keep chat, remove markers and the "no chat alternative"
144
+ const markers = [
145
+ /\/\/\s*\{CHAT_START\}/g,
146
+ /\/\/\s*\{CHAT_END\}/g,
147
+ /\/\/\s*\{NO_CHAT_START\}/g,
148
+ /\/\/\s*\{NO_CHAT_END\}/g,
149
+ /\{\s*\/\*\s*\{CHAT_START\}\s*\*\/\s*\}/g,
150
+ /\{\s*\/\*\s*\{CHAT_END\}\s*\*\/\s*\}/g
151
+ ];
152
+ markers.forEach(re => {
153
+ appContent = appContent.replace(re, "");
154
+ });
155
+
156
+ // Remove the "no chat" blocks entirely
157
+ appContent = appContent.replace(/\{\s*\/\*\s*\{NO_CHAT_START\}[\s\S]*?\{NO_CHAT_END\}\s*\*\/\s*\}/g, "");
158
+ } else {
159
+ // Remove chat blocks, uncomment "no chat alternative"
160
+ appContent = appContent.replace(/\/\/\s*\{CHAT_START\}[\s\S]*?\/\/\s*\{CHAT_END\}\n?/g, "");
161
+ appContent = appContent.replace(/\{\s*\/\*\s*\{CHAT_START\}\s*\*\/\s*\}[\s\S]*?\{\s*\/\*\s*\{CHAT_END\}\s*\*\/\s*\}\n?/g, "");
162
+
163
+ // Uncomment {NO_CHAT_START} ... {NO_CHAT_END}
164
+ // Case 1: Multi-line JSX comment
165
+ appContent = appContent.replace(/\{\s*\/\*\s*\{NO_CHAT_START\}\n?|(?:\n?\s*)?\{NO_CHAT_END\}\s*\*\/\s*\}/g, "");
166
+ // Case 2: single-line JS comment
167
+ appContent = appContent.replace(/\/\/\s*\{NO_CHAT_START\}\n?|\/\/\s*\{NO_CHAT_END\}\n?/g, "");
168
+ }
169
+ await fs.writeFile(appPath, appContent);
170
+ }
171
+
120
172
  // Generate package.json with project name
121
173
  const pkgTemplatePath = path.join(targetDir, "package.json.template");
122
174
  const pkgTargetPath = path.join(targetDir, "package.json");
@@ -124,6 +176,14 @@ async function main() {
124
176
  if (fs.existsSync(pkgTemplatePath)) {
125
177
  let pkgContent = await fs.readFile(pkgTemplatePath, "utf-8");
126
178
  pkgContent = pkgContent.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
179
+
180
+ // Add chat widget dependency if requested
181
+ if (includeChatWidget) {
182
+ const pkg = JSON.parse(pkgContent);
183
+ pkg.dependencies["@cundi/react-chat-widget"] = "^1.0.0";
184
+ pkgContent = JSON.stringify(pkg, null, 2);
185
+ }
186
+
127
187
  await fs.writeFile(pkgTargetPath, pkgContent);
128
188
  await fs.remove(pkgTemplatePath);
129
189
  }
@@ -141,6 +201,9 @@ VITE_KEYCLOAK_URL=http://localhost:8080
141
201
  VITE_KEYCLOAK_REALM=your-realm
142
202
  VITE_KEYCLOAK_CLIENT_ID=your-client-id
143
203
  VITE_REDIRECT_URI=http://localhost:5173/auth/callback
204
+
205
+ # Chat Widget Configuration (Optional)
206
+ VITE_CHAT_WEBHOOK_URL=
144
207
  `
145
208
  );
146
209
  }
@@ -171,6 +234,10 @@ VITE_REDIRECT_URI=http://localhost:5173/auth/callback
171
234
  console.log(kleur.yellow("Note: Sample pages are included. Make sure your backend has the corresponding Business Objects."));
172
235
  console.log();
173
236
  }
237
+ if (includeChatWidget) {
238
+ console.log(kleur.yellow("Note: Chat Widget is included. See the Chat Demo page for usage examples."));
239
+ console.log();
240
+ }
174
241
  console.log(kleur.gray("────────────────────────────────────"));
175
242
  console.log(kleur.blue("Happy coding! 🎉"));
176
243
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-cundi-app",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "Create a new Cundi app with React + Refine + Ant Design",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,7 +24,8 @@
24
24
  "dependencies": {
25
25
  "@ant-design/icons": "^5.5.1",
26
26
  "@ant-design/v5-patch-for-react-19": "^1.0.3",
27
- "@cundi/refine-xaf": "^1.0.14",
27
+ "@cundi/refine-xaf": "^1.0.15",
28
+ "@cundi/react-chat-widget": "^1.0.1",
28
29
  "@refinedev/antd": "^6.0.3",
29
30
  "@refinedev/cli": "^2.16.50",
30
31
  "@refinedev/core": "^5.0.6",
@@ -34,7 +34,9 @@ import { DashboardPage } from "./pages/dashboard";
34
34
  import {
35
35
  authProvider,
36
36
  dataProvider,
37
+ // {NO_CHAT_START}
37
38
  Header,
39
+ // {NO_CHAT_END}
38
40
  LoginPage,
39
41
  KeycloakLoginPage,
40
42
  AuthCallback,
@@ -57,6 +59,11 @@ import {
57
59
 
58
60
  import { accessControlProvider } from "./accessControlProvider";
59
61
 
62
+ // {CHAT_START}
63
+ import { ChatProvider } from "./components/ChatPanel";
64
+ import { HeaderWithChat } from "./components/HeaderWithChat";
65
+ // {CHAT_END}
66
+
60
67
  const API_URL = import.meta.env.VITE_API_URL + "/api/odata";
61
68
 
62
69
  const InnerApp: React.FC = () => {
@@ -169,7 +176,12 @@ const InnerApp: React.FC = () => {
169
176
  key="authenticated-routes"
170
177
  fallback={<CatchAllNavigate to="/login" />}
171
178
  >
179
+ { /* {CHAT_START} */}
180
+ <ThemedLayout Header={HeaderWithChat}>
181
+ { /* {CHAT_END} */}
182
+ { /* {NO_CHAT_START}
172
183
  <ThemedLayout Header={Header}>
184
+ {NO_CHAT_END} */ }
173
185
  <Outlet />
174
186
  </ThemedLayout>
175
187
  </Authenticated>
@@ -221,7 +233,12 @@ const InnerApp: React.FC = () => {
221
233
  <Route
222
234
  element={
223
235
  <Authenticated key="catch-all">
236
+ { /* {CHAT_START} */}
237
+ <ThemedLayout Header={HeaderWithChat}>
238
+ { /* {CHAT_END} */}
239
+ { /* {NO_CHAT_START}
224
240
  <ThemedLayout Header={Header}>
241
+ {NO_CHAT_END} */ }
225
242
  <Outlet />
226
243
  </ThemedLayout>
227
244
  </Authenticated>
@@ -241,8 +258,16 @@ const InnerApp: React.FC = () => {
241
258
  const App: React.FC = () => {
242
259
  return (
243
260
  <ColorModeContextProvider>
261
+ {/* {CHAT_START} */}
262
+ <ChatProvider>
263
+ <InnerApp />
264
+ </ChatProvider>
265
+ {/* {CHAT_END} */}
266
+ {/* {NO_CHAT_START}
244
267
  <InnerApp />
268
+ {NO_CHAT_END} */}
245
269
  </ColorModeContextProvider>
270
+
246
271
  );
247
272
  };
248
273
 
@@ -35,7 +35,9 @@ import { DashboardPage } from "./pages/dashboard";
35
35
  import {
36
36
  authProvider,
37
37
  dataProvider,
38
+ // {NO_CHAT_START}
38
39
  Header,
40
+ // {NO_CHAT_END}
39
41
  LoginPage,
40
42
  KeycloakLoginPage,
41
43
  AuthCallback,
@@ -76,6 +78,12 @@ import { DrawioTestList } from "./pages/samples/drawio-test/list";
76
78
  import { DrawioTestCreate } from "./pages/samples/drawio-test/create";
77
79
  import { DrawioTestEdit } from "./pages/samples/drawio-test/edit";
78
80
  import { DrawioTestShow } from "./pages/samples/drawio-test/show";
81
+ import { OrderList, OrderCreate, OrderEdit, OrderShow } from "./pages/samples/order";
82
+
83
+ // {CHAT_START}
84
+ import { ChatProvider } from "./components/ChatPanel";
85
+ import { HeaderWithChat } from "./components/HeaderWithChat";
86
+ // {CHAT_END}
79
87
 
80
88
  const API_URL = import.meta.env.VITE_API_URL + "/api/odata";
81
89
 
@@ -131,6 +139,17 @@ const InnerApp: React.FC = () => {
131
139
  parent: "Model",
132
140
  },
133
141
  },
142
+ {
143
+ name: "Order",
144
+ list: "/Orders",
145
+ create: "/Orders/create",
146
+ edit: "/Orders/edit/:id",
147
+ show: "/Orders/show/:id",
148
+ meta: {
149
+ label: t("sider.order"),
150
+ parent: "Model",
151
+ },
152
+ },
134
153
  {
135
154
  name: "TiptapTest",
136
155
  list: "/TiptapTests",
@@ -264,7 +283,12 @@ const InnerApp: React.FC = () => {
264
283
  key="authenticated-routes"
265
284
  fallback={<CatchAllNavigate to="/login" />}
266
285
  >
286
+ { /* {CHAT_START} */}
287
+ <ThemedLayout Header={HeaderWithChat}>
288
+ { /* {CHAT_END} */}
289
+ { /* {NO_CHAT_START}
267
290
  <ThemedLayout Header={Header}>
291
+ {NO_CHAT_END} */ }
268
292
  <Outlet />
269
293
  </ThemedLayout>
270
294
  </Authenticated>
@@ -305,6 +329,13 @@ const InnerApp: React.FC = () => {
305
329
  <Route path="show/:id" element={<DrawioTestShow />} />
306
330
  </Route>
307
331
 
332
+ <Route path="/Orders">
333
+ <Route index element={<OrderList />} />
334
+ <Route path="create" element={<OrderCreate />} />
335
+ <Route path="edit/:id" element={<OrderEdit />} />
336
+ <Route path="show/:id" element={<OrderShow />} />
337
+ </Route>
338
+
308
339
  {/* Core routes */}
309
340
  <Route path="/ApplicationUsers">
310
341
  <Route index element={<ApplicationUserList />} />
@@ -350,7 +381,12 @@ const InnerApp: React.FC = () => {
350
381
  <Route
351
382
  element={
352
383
  <Authenticated key="catch-all">
353
- <ThemedLayout Header={Header}>
384
+ { /* {CHAT_START} */}
385
+ <ThemedLayout Header={HeaderWithChat}>
386
+ { /* {CHAT_END} */}
387
+ { /* {NO_CHAT_START}
388
+ <ThemedLayout Header={Header}>
389
+ {NO_CHAT_END} */ }
354
390
  <Outlet />
355
391
  </ThemedLayout>
356
392
  </Authenticated>
@@ -362,16 +398,24 @@ const InnerApp: React.FC = () => {
362
398
  <UnsavedChangesNotifier />
363
399
  <DocumentTitleHandler />
364
400
  </Refine>
365
- </AntdApp>
366
- </BrowserRouter>
401
+ </AntdApp >
402
+ </BrowserRouter >
367
403
  );
368
404
  };
369
405
 
370
406
  const App: React.FC = () => {
371
407
  return (
372
408
  <ColorModeContextProvider>
409
+ {/* {CHAT_START} */}
410
+ <ChatProvider>
411
+ <InnerApp />
412
+ </ChatProvider>
413
+ {/* {CHAT_END} */}
414
+ {/* {NO_CHAT_START}
373
415
  <InnerApp />
416
+ {NO_CHAT_END} */}
374
417
  </ColorModeContextProvider>
418
+
375
419
  );
376
420
  };
377
421
 
@@ -0,0 +1,239 @@
1
+ import React, { createContext, useContext, useState, ReactNode } from "react";
2
+ import { Button, Tooltip, theme } from "antd";
3
+ import { MessageOutlined, CloseOutlined } from "@ant-design/icons";
4
+ import { ChatWidget } from "@cundi/react-chat-widget";
5
+ import "@cundi/react-chat-widget/styles.css";
6
+ import { useTranslation } from "react-i18next";
7
+
8
+ const { useToken } = theme;
9
+
10
+ // ============ Chat Context ============
11
+ interface ChatContextType {
12
+ isOpen: boolean;
13
+ open: () => void;
14
+ close: () => void;
15
+ toggle: () => void;
16
+ }
17
+
18
+ const ChatContext = createContext<ChatContextType | null>(null);
19
+
20
+ export const useChatPanel = () => {
21
+ const context = useContext(ChatContext);
22
+ if (!context) {
23
+ throw new Error("useChatPanel must be used within ChatProvider");
24
+ }
25
+ return context;
26
+ };
27
+
28
+ // ============ Mock Data for Demo ============
29
+ const MOCK_CHAT_RESPONSES: Record<string, (message: string, t: any) => any> = {
30
+ help: (_, t) => ({
31
+ output: `## 測試指令說明
32
+ 您可以輸入以下關鍵字來測試不同功能:
33
+
34
+ | 指令 | 說明 |
35
+ |------|------|
36
+ | \`markdown\` | 顯示 Markdown 格式範例 |
37
+ | \`table\` | 顯示表格範例 |
38
+ | \`image\` | 顯示圖片範例 |
39
+ | \`file\` | 顯示檔案下載連結 |
40
+ | \`code\` | 顯示程式碼區塊範例 |
41
+ | \`list\` | 顯示清單範例 |
42
+ | \`help\` | 顯示此說明 |`
43
+ }),
44
+ markdown: () => ({
45
+ output: `### Markdown 測試
46
+ 這是一個 **Markdown** 格式的回應!
47
+ - 支援 **粗體** 和 *斜體*
48
+ - 支援 \`程式碼\` 區塊
49
+ - 支援 [連結](https://google.com)
50
+ > 這是一段引用文字`
51
+ }),
52
+ table: () => ({
53
+ output: `### 產品清單表格
54
+ | 產品名稱 | 價格 | 庫存 |
55
+ |---------|------|------|
56
+ | 筆記型電腦 | $999 | 15 |
57
+ | 無線滑鼠 | $29 | 150 |
58
+ | 機械鍵盤 | $149 | 0 |`
59
+ }),
60
+ image: () => ({
61
+ output: `### 圖片展示\n這是一張隨機範例圖片:`,
62
+ image: 'https://picsum.photos/400/300'
63
+ }),
64
+ file: () => ({
65
+ output: `### 檔案下載\n以下是可供下載的檔案:`,
66
+ files: [
67
+ {
68
+ name: '範例文件.pdf',
69
+ url: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf',
70
+ type: 'application/pdf',
71
+ size: 13264,
72
+ }
73
+ ]
74
+ }),
75
+ code: () => ({
76
+ output: `### 程式碼範例\n\`\`\`tsx
77
+ const HelloWorld = () => {
78
+ return <div>Hello Cundi!</div>;
79
+ };
80
+ \`\`\``
81
+ }),
82
+ list: () => ({
83
+ output: `### 開發進度\n- [x] 建立專案
84
+ - [x] 整合聊天面板
85
+ - [ ] 介面優化
86
+ - [ ] 發布正式版`
87
+ })
88
+ };
89
+
90
+ // ============ Chat Provider ============
91
+ interface ChatProviderProps {
92
+ children: ReactNode;
93
+ /** Width of chat panel */
94
+ panelWidth?: number;
95
+ /** Custom message handler for chat */
96
+ onSendMessage?: (message: string, files?: File[]) => Promise<{ output: string } | string>;
97
+ /** Webhook URL for webhook mode */
98
+ webhookUrl?: string;
99
+ /** Session ID for webhook mode */
100
+ sessionId?: string;
101
+ }
102
+
103
+ export const ChatProvider: React.FC<ChatProviderProps> = ({
104
+ children,
105
+ panelWidth = 500,
106
+ onSendMessage,
107
+ webhookUrl,
108
+ sessionId,
109
+ }) => {
110
+ const [isOpen, setIsOpen] = useState(false);
111
+ const { token } = useToken();
112
+ const { t } = useTranslation();
113
+
114
+ // Default message handler (mock response generator)
115
+ const defaultMessageHandler = async (message: string) => {
116
+ await new Promise((resolve) => setTimeout(resolve, 800));
117
+ const input = message.toLowerCase().trim();
118
+
119
+ for (const [keyword, generator] of Object.entries(MOCK_CHAT_RESPONSES)) {
120
+ if (input.includes(keyword)) {
121
+ return generator(message, t);
122
+ }
123
+ }
124
+
125
+ return {
126
+ output: t("chat.defaultResponse", `收到您的訊息:「${message}」\n\n這是一個示範回應。您可以輸入 \`help\` 來查看更多指令測試。`),
127
+ };
128
+ };
129
+
130
+ const contextValue: ChatContextType = {
131
+ isOpen,
132
+ open: () => setIsOpen(true),
133
+ close: () => setIsOpen(false),
134
+ toggle: () => setIsOpen((prev) => !prev),
135
+ };
136
+
137
+ return (
138
+ <ChatContext.Provider value={contextValue}>
139
+ {/* Main content with margin adjustment */}
140
+ <div
141
+ style={{
142
+ marginRight: isOpen ? panelWidth : 0,
143
+ transition: "margin-right 0.3s ease-in-out",
144
+ minHeight: "100vh",
145
+ }}
146
+ >
147
+ {children}
148
+ </div>
149
+
150
+ {/* Fixed Chat Panel */}
151
+ <div
152
+ style={{
153
+ position: "fixed",
154
+ top: 0,
155
+ right: 0,
156
+ bottom: 0,
157
+ width: isOpen ? panelWidth : 0,
158
+ backgroundColor: token.colorBgContainer,
159
+ borderLeft: isOpen ? `1px solid ${token.colorBorderSecondary}` : "none",
160
+ transition: "width 0.3s ease-in-out",
161
+ overflow: "hidden",
162
+ zIndex: 1000,
163
+ display: "flex",
164
+ flexDirection: "column",
165
+ }}
166
+ >
167
+ {isOpen && (
168
+ <>
169
+ {/* Panel Header */}
170
+ <div
171
+ style={{
172
+ display: "flex",
173
+ alignItems: "center",
174
+ justifyContent: "space-between",
175
+ padding: "12px 16px",
176
+ borderBottom: `1px solid ${token.colorBorderSecondary}`,
177
+ backgroundColor: token.colorBgElevated,
178
+ height: 64,
179
+ }}
180
+ >
181
+ <span style={{ fontWeight: 600, fontSize: 16, color: token.colorText }}>
182
+ {t("chat.panelTitle", "AI Assistant")}
183
+ </span>
184
+ <Button
185
+ type="text"
186
+ icon={<CloseOutlined />}
187
+ onClick={() => setIsOpen(false)}
188
+ size="small"
189
+ />
190
+ </div>
191
+
192
+ {/* Chat Widget */}
193
+ <div style={{ flex: 1, minHeight: 0 }}>
194
+ {webhookUrl ? (
195
+ <ChatWidget
196
+ webhookUrl={webhookUrl}
197
+ sessionId={sessionId || `session-${Date.now()}`}
198
+ showHeader={false}
199
+ placeholder={t("chat.placeholder", "Type a message...")}
200
+ user={{ name: t("chat.user", "You"), avatar: "👤" }}
201
+ assistant={{ name: t("chat.assistant", "AI"), avatar: "🤖" }}
202
+ />
203
+ ) : (
204
+ <ChatWidget
205
+ onSendMessage={onSendMessage || defaultMessageHandler}
206
+ showHeader={false}
207
+ placeholder={t("chat.placeholder", "Type a message...")}
208
+ user={{ name: t("chat.user", "You"), avatar: "👤" }}
209
+ assistant={{ name: t("chat.assistant", "AI"), avatar: "🤖" }}
210
+ allowFileUpload={true}
211
+ />
212
+ )}
213
+ </div>
214
+ </>
215
+ )}
216
+ </div>
217
+ </ChatContext.Provider>
218
+ );
219
+ };
220
+
221
+ // ============ Chat Toggle Button ============
222
+ /**
223
+ * Button component to toggle the chat panel.
224
+ * Place this anywhere in your app (e.g., in the Header).
225
+ */
226
+ export const ChatToggleButton: React.FC = () => {
227
+ const { isOpen, toggle } = useChatPanel();
228
+ const { t } = useTranslation();
229
+
230
+ return (
231
+ <Tooltip title={t("chat.toggleTooltip", "Toggle AI Chat")}>
232
+ <Button
233
+ type={isOpen ? "primary" : "text"}
234
+ icon={<MessageOutlined />}
235
+ onClick={toggle}
236
+ />
237
+ </Tooltip>
238
+ );
239
+ };
@@ -0,0 +1,181 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { useLogout, useGetIdentity, useUpdatePassword } from "@refinedev/core";
3
+ import { Layout, Button, Space, Typography, Avatar, theme, Dropdown, MenuProps, Tooltip } from "antd";
4
+ import { LogoutOutlined, UserOutlined, DownOutlined, SunOutlined, MoonOutlined, CameraOutlined, LockOutlined, GlobalOutlined, MessageOutlined } from "@ant-design/icons";
5
+ import { useColorMode, GlobalSearch, ChangePhotoModal, ChangePasswordModal } from "@cundi/refine-xaf";
6
+ import { useTranslation } from "react-i18next";
7
+ import { useChatPanel } from "./ChatPanel";
8
+
9
+ const { Text } = Typography;
10
+ const { useToken } = theme;
11
+
12
+ /**
13
+ * Custom Header that includes the chat toggle button
14
+ * This is a modified version of the Header from @cundi/refine-xaf
15
+ */
16
+ export const HeaderWithChat: React.FC = () => {
17
+ const { mutate: logout } = useLogout();
18
+ const { mutate: updatePassword } = useUpdatePassword();
19
+ const { data: user } = useGetIdentity<any>();
20
+ const { mode, setMode } = useColorMode();
21
+ const { token } = useToken();
22
+ const { isOpen: isChatOpen, toggle: toggleChat } = useChatPanel();
23
+
24
+ const { t: translate, i18n } = useTranslation();
25
+ const currentLocale = i18n.language;
26
+
27
+ const handleLanguageChange = (lang: string) => {
28
+ i18n.changeLanguage(lang);
29
+ };
30
+
31
+ // Track auth provider
32
+ const [authProvider, setAuthProvider] = useState<string | null>(null);
33
+
34
+ useEffect(() => {
35
+ if (user) {
36
+ setAuthProvider(localStorage.getItem("auth_provider"));
37
+ }
38
+ }, [user]);
39
+
40
+ // Photo Modal State
41
+ const [isPhotoModalOpen, setIsPhotoModalOpen] = useState(false);
42
+
43
+ // Password Modal State
44
+ const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false);
45
+
46
+ const menuItems: MenuProps["items"] = [
47
+ {
48
+ key: "user-info",
49
+ label: (
50
+ <Space direction="vertical" size={0}>
51
+ <Text strong>{user?.name}</Text>
52
+ </Space>
53
+ ),
54
+ icon: <UserOutlined />,
55
+ disabled: true,
56
+ style: { cursor: "default", color: token.colorText },
57
+ },
58
+ {
59
+ type: "divider",
60
+ },
61
+ {
62
+ key: "change-photo",
63
+ label: translate("components.header.menu.changePhoto", "Change Photo"),
64
+ icon: <CameraOutlined />,
65
+ onClick: () => {
66
+ setIsPhotoModalOpen(true);
67
+ },
68
+ },
69
+ {
70
+ key: "change-password",
71
+ label: authProvider === "keycloak"
72
+ ? translate("components.header.menu.manageAccount", "Manage Account in Keycloak")
73
+ : translate("components.header.menu.changePassword", "Change Password"),
74
+ icon: <LockOutlined />,
75
+ onClick: () => {
76
+ if (authProvider === "keycloak") {
77
+ const env = (import.meta as any).env;
78
+ const keycloakUrl = env?.VITE_KEYCLOAK_URL || "http://localhost:8080";
79
+ const realm = env?.VITE_KEYCLOAK_REALM || "cundi";
80
+ window.open(`${keycloakUrl}/realms/${realm}/account`, "_blank");
81
+ } else {
82
+ setIsPasswordModalOpen(true);
83
+ }
84
+ },
85
+ },
86
+ {
87
+ type: "divider",
88
+ },
89
+ {
90
+ key: "theme",
91
+ label: mode === "light"
92
+ ? translate("components.header.theme.dark", "Dark Theme")
93
+ : translate("components.header.theme.light", "Light Theme"),
94
+ icon: mode === "light" ? <MoonOutlined /> : <SunOutlined />,
95
+ onClick: () => setMode(mode === "light" ? "dark" : "light"),
96
+ },
97
+ {
98
+ key: "logout",
99
+ label: translate("components.header.menu.logout", "Logout"),
100
+ icon: <LogoutOutlined />,
101
+ onClick: () => logout(),
102
+ }
103
+ ];
104
+
105
+ return (
106
+ <Layout.Header
107
+ style={{
108
+ display: "flex",
109
+ justifyContent: "flex-end",
110
+ alignItems: "center",
111
+ padding: "0 24px",
112
+ height: "64px",
113
+ backgroundColor: token.colorBgElevated,
114
+ position: "sticky",
115
+ top: 0,
116
+ zIndex: 1,
117
+ }}
118
+ >
119
+ <Space size="middle">
120
+ <div>
121
+ <GlobalSearch width={300} placeholder={translate("components.globalSearch.placeholder", "Search...")} />
122
+ </div>
123
+
124
+ {/* Chat Toggle Button */}
125
+ <Tooltip title={translate("chat.toggleTooltip", "Toggle AI Chat")}>
126
+ <Button
127
+ type={isChatOpen ? "primary" : "text"}
128
+ icon={<MessageOutlined />}
129
+ onClick={toggleChat}
130
+ />
131
+ </Tooltip>
132
+
133
+ <Dropdown
134
+ menu={{
135
+ items: [
136
+ {
137
+ key: "en",
138
+ label: translate("components.header.language.en", "English"),
139
+ onClick: () => handleLanguageChange("en"),
140
+ disabled: currentLocale === "en",
141
+ },
142
+ {
143
+ key: "zh-TW",
144
+ label: translate("components.header.language.zh-TW", "繁體中文"),
145
+ onClick: () => handleLanguageChange("zh-TW"),
146
+ disabled: currentLocale === "zh-TW",
147
+ },
148
+ ]
149
+ }}
150
+ trigger={["click"]}
151
+ >
152
+ <Button type="text" icon={<GlobalOutlined />}>
153
+ {currentLocale === "zh-TW" ? "繁體中文" : "English"} <DownOutlined />
154
+ </Button>
155
+ </Dropdown>
156
+
157
+ <Dropdown menu={{ items: menuItems }} trigger={["click"]}>
158
+ <Button type="text" style={{ height: 48 }}>
159
+ <Space>
160
+ <Avatar src={user?.avatar} alt={user?.name} icon={<UserOutlined />} />
161
+ <Text>{user?.name}</Text>
162
+ <DownOutlined style={{ fontSize: 12 }} />
163
+ </Space>
164
+ </Button>
165
+ </Dropdown>
166
+ </Space>
167
+
168
+ {/* Photo Modal */}
169
+ <ChangePhotoModal
170
+ open={isPhotoModalOpen}
171
+ onClose={() => setIsPhotoModalOpen(false)}
172
+ user={user}
173
+ />
174
+
175
+ <ChangePasswordModal
176
+ open={isPasswordModalOpen}
177
+ onClose={() => setIsPasswordModalOpen(false)}
178
+ />
179
+ </Layout.Header>
180
+ );
181
+ };
@@ -14,6 +14,14 @@ const enExtended = {
14
14
  triggerLogs: "Trigger Logs",
15
15
  mirrorConfigs: "Mirror Configs",
16
16
  },
17
+ // Chat translations
18
+ chat: {
19
+ panelTitle: "AI Assistant",
20
+ toggleTooltip: "Toggle AI Chat",
21
+ placeholder: "Type a message...",
22
+ user: "You",
23
+ assistant: "AI",
24
+ },
17
25
  };
18
26
 
19
27
  const zhTWExtended = {
@@ -24,6 +32,14 @@ const zhTWExtended = {
24
32
  triggerLogs: "觸發紀錄",
25
33
  mirrorConfigs: "鏡射設定",
26
34
  },
35
+ // Chat translations
36
+ chat: {
37
+ panelTitle: "AI 助手",
38
+ toggleTooltip: "開啟/關閉 AI 聊天",
39
+ placeholder: "輸入訊息...",
40
+ user: "您",
41
+ assistant: "AI",
42
+ },
27
43
  };
28
44
 
29
45
  i18n
@@ -17,10 +17,19 @@ const enExtended = {
17
17
  samples: "Samples",
18
18
  model: "Model",
19
19
  basicTypeTest: "Basic Type",
20
+ order: "Master-Details",
20
21
  tiptapTest: "Tiptap Editor",
21
22
  drawioTest: "Drawio Editor",
22
23
  fullTextSearch: "Full Text Search",
23
24
  },
25
+ // Chat translations
26
+ chat: {
27
+ panelTitle: "AI Assistant",
28
+ toggleTooltip: "Toggle AI Chat",
29
+ placeholder: "Type a message...",
30
+ user: "You",
31
+ assistant: "AI",
32
+ },
24
33
  // Sample field translations
25
34
  article: {
26
35
  title: "Title",
@@ -45,12 +54,39 @@ const enExtended = {
45
54
  doubleValue: "Double Value",
46
55
  dateTimeValue: "DateTime Value",
47
56
  boolValue: "Bool Value",
57
+ statusValue: "Status",
58
+ status: {
59
+ Draft: "Draft",
60
+ Active: "Active",
61
+ Completed: "Completed",
62
+ Cancelled: "Cancelled",
63
+ },
48
64
  imageValue: "Image Value",
49
65
  },
66
+ order: {
67
+ orderNumber: "Order Number",
68
+ orderDate: "Order Date",
69
+ customerName: "Customer Name",
70
+ notes: "Notes",
71
+ totalAmount: "Total Amount",
72
+ items: "Order Items",
73
+ },
74
+ orderItem: {
75
+ title: "Order Item",
76
+ productName: "Product Name",
77
+ quantity: "Quantity",
78
+ unitPrice: "Unit Price",
79
+ subtotal: "Subtotal",
80
+ },
81
+ validation: {
82
+ required: "This field is required",
83
+ },
50
84
  types: {
51
85
  Article: "Article",
52
86
  Product: "Product",
53
87
  BasicTypeTest: "Basic Type",
88
+ Order: "Order",
89
+ OrderItem: "Order Item",
54
90
  },
55
91
  };
56
92
 
@@ -65,10 +101,19 @@ const zhTWExtended = {
65
101
  samples: "範例",
66
102
  model: "資料模型",
67
103
  basicTypeTest: "基本型別",
104
+ order: "主從式資料",
68
105
  tiptapTest: "Tiptap 編輯器",
69
106
  drawioTest: "Drawio 編輯器",
70
107
  fullTextSearch: "全文檢索",
71
108
  },
109
+ // Chat translations
110
+ chat: {
111
+ panelTitle: "AI 助手",
112
+ toggleTooltip: "開啟/關閉 AI 聊天",
113
+ placeholder: "輸入訊息...",
114
+ user: "您",
115
+ assistant: "AI",
116
+ },
72
117
  // Sample field translations
73
118
  article: {
74
119
  title: "標題",
@@ -93,12 +138,39 @@ const zhTWExtended = {
93
138
  doubleValue: "浮點數",
94
139
  dateTimeValue: "日期時間",
95
140
  boolValue: "布林值",
141
+ statusValue: "狀態",
142
+ status: {
143
+ Draft: "草稿",
144
+ Active: "啟用",
145
+ Completed: "完成",
146
+ Cancelled: "取消",
147
+ },
96
148
  imageValue: "圖片",
97
149
  },
150
+ order: {
151
+ orderNumber: "訂單編號",
152
+ orderDate: "訂單日期",
153
+ customerName: "客戶名稱",
154
+ notes: "備註",
155
+ totalAmount: "總金額",
156
+ items: "訂單明細",
157
+ },
158
+ orderItem: {
159
+ title: "訂單明細",
160
+ productName: "產品名稱",
161
+ quantity: "數量",
162
+ unitPrice: "單價",
163
+ subtotal: "小計",
164
+ },
165
+ validation: {
166
+ required: "此欄位為必填",
167
+ },
98
168
  types: {
99
169
  Article: "文章",
100
170
  Product: "產品",
101
171
  BasicTypeTest: "基本型別",
172
+ Order: "訂單",
173
+ OrderItem: "訂單明細",
102
174
  },
103
175
  };
104
176
 
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { Create, useForm } from "@refinedev/antd";
3
- import { Form, Input, InputNumber, DatePicker, Switch } from "antd";
3
+ import { Form, Input, InputNumber, DatePicker, Switch, Select } from "antd";
4
4
  import { useTranslation } from "react-i18next";
5
5
  import { Base64Upload } from "@cundi/refine-xaf";
6
6
  import dayjs from "dayjs";
@@ -56,6 +56,19 @@ export const BasicTypeTestCreate: React.FC = () => {
56
56
  >
57
57
  <Switch />
58
58
  </Form.Item>
59
+ <Form.Item
60
+ label={t("basicTypeTest.statusValue")}
61
+ name="StatusValue"
62
+ >
63
+ <Select
64
+ options={[
65
+ { value: "Draft", label: t("basicTypeTest.status.Draft") },
66
+ { value: "Active", label: t("basicTypeTest.status.Active") },
67
+ { value: "Completed", label: t("basicTypeTest.status.Completed") },
68
+ { value: "Cancelled", label: t("basicTypeTest.status.Cancelled") },
69
+ ]}
70
+ />
71
+ </Form.Item>
59
72
  <Form.Item
60
73
  label={t("basicTypeTest.imageValue")}
61
74
  name="ImageValue"
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { Edit, useForm } from "@refinedev/antd";
3
- import { Form, Input, InputNumber, DatePicker, Switch } from "antd";
3
+ import { Form, Input, InputNumber, DatePicker, Switch, Select } from "antd";
4
4
  import { useTranslation } from "react-i18next";
5
5
  import { Base64Upload } from "@cundi/refine-xaf";
6
6
  import dayjs from "dayjs";
@@ -56,6 +56,19 @@ export const BasicTypeTestEdit: React.FC = () => {
56
56
  >
57
57
  <Switch />
58
58
  </Form.Item>
59
+ <Form.Item
60
+ label={t("basicTypeTest.statusValue")}
61
+ name="StatusValue"
62
+ >
63
+ <Select
64
+ options={[
65
+ { value: "Draft", label: t("basicTypeTest.status.Draft") },
66
+ { value: "Active", label: t("basicTypeTest.status.Active") },
67
+ { value: "Completed", label: t("basicTypeTest.status.Completed") },
68
+ { value: "Cancelled", label: t("basicTypeTest.status.Cancelled") },
69
+ ]}
70
+ />
71
+ </Form.Item>
59
72
  <Form.Item
60
73
  label={t("basicTypeTest.imageValue")}
61
74
  name="ImageValue"
@@ -46,6 +46,22 @@ export const BasicTypeTestList: React.FC = () => {
46
46
  // @ts-ignore
47
47
  defaultVisible
48
48
  />
49
+ <Table.Column
50
+ dataIndex="StatusValue"
51
+ title={t("basicTypeTest.statusValue")}
52
+ sorter
53
+ render={(value: string) => (
54
+ <Tag color={
55
+ value === "Active" ? "green" :
56
+ value === "Completed" ? "blue" :
57
+ value === "Cancelled" ? "red" : "default"
58
+ }>
59
+ {t(`basicTypeTest.status.${value}`, value)}
60
+ </Tag>
61
+ )}
62
+ // @ts-ignore
63
+ defaultVisible
64
+ />
49
65
  <Table.Column
50
66
  dataIndex="ImageValue"
51
67
  title={t("basicTypeTest.imageValue")}
@@ -0,0 +1,50 @@
1
+ import React from "react";
2
+ import { Create, useForm } from "@refinedev/antd";
3
+ import { Form, Input, DatePicker } from "antd";
4
+ import { useTranslation } from "react-i18next";
5
+ import dayjs from "dayjs";
6
+
7
+ export const OrderCreate: React.FC = () => {
8
+ const { t } = useTranslation();
9
+ const { formProps, saveButtonProps } = useForm({
10
+ redirect: "edit",
11
+ });
12
+
13
+ return (
14
+ <Create saveButtonProps={saveButtonProps}>
15
+ <Form {...formProps} layout="vertical">
16
+ <Form.Item
17
+ label={t("order.orderNumber")}
18
+ name="OrderNumber"
19
+ rules={[{ required: true, message: t("validation.required") }]}
20
+ >
21
+ <Input />
22
+ </Form.Item>
23
+ <Form.Item
24
+ label={t("order.orderDate")}
25
+ name="OrderDate"
26
+ getValueProps={(value) => ({
27
+ value: value ? dayjs(value) : dayjs(),
28
+ })}
29
+ getValueFromEvent={(date) => date?.toISOString()}
30
+ rules={[{ required: true, message: t("validation.required") }]}
31
+ >
32
+ <DatePicker style={{ width: "100%" }} />
33
+ </Form.Item>
34
+ <Form.Item
35
+ label={t("order.customerName")}
36
+ name="CustomerName"
37
+ rules={[{ required: true, message: t("validation.required") }]}
38
+ >
39
+ <Input />
40
+ </Form.Item>
41
+ <Form.Item
42
+ label={t("order.notes")}
43
+ name="Notes"
44
+ >
45
+ <Input.TextArea rows={3} />
46
+ </Form.Item>
47
+ </Form>
48
+ </Create>
49
+ );
50
+ };
@@ -0,0 +1,139 @@
1
+ import React from "react";
2
+ import { Edit, useForm } from "@refinedev/antd";
3
+ import { Form, Input, DatePicker, InputNumber, Card } from "antd";
4
+ import { useTranslation } from "react-i18next";
5
+ import { RelatedList } from "@cundi/refine-xaf";
6
+ import { useTable } from "@refinedev/antd";
7
+ import { Table } from "antd";
8
+ import dayjs from "dayjs";
9
+
10
+ // OrderItem Form Fields Component
11
+ const OrderItemFormFields: React.FC<{ mode: "create" | "edit" }> = () => {
12
+ const { t } = useTranslation();
13
+
14
+ return (
15
+ <>
16
+ <Form.Item
17
+ label={t("orderItem.productName")}
18
+ name="ProductName"
19
+ rules={[{ required: true, message: t("validation.required") }]}
20
+ >
21
+ <Input />
22
+ </Form.Item>
23
+ <Form.Item
24
+ label={t("orderItem.quantity")}
25
+ name="Quantity"
26
+ rules={[{ required: true, message: t("validation.required") }]}
27
+ >
28
+ <InputNumber min={1} style={{ width: "100%" }} />
29
+ </Form.Item>
30
+ <Form.Item
31
+ label={t("orderItem.unitPrice")}
32
+ name="UnitPrice"
33
+ rules={[{ required: true, message: t("validation.required") }]}
34
+ >
35
+ <InputNumber min={0} precision={2} style={{ width: "100%" }} prefix="$" />
36
+ </Form.Item>
37
+ </>
38
+ );
39
+ };
40
+
41
+ export const OrderEdit: React.FC = () => {
42
+ const { t } = useTranslation();
43
+ const { formProps, saveButtonProps, query } = useForm({
44
+ meta: {
45
+ expand: ["Items"],
46
+ },
47
+ });
48
+
49
+ const orderId = query?.data?.data?.Oid as string | undefined;
50
+
51
+ // Fetch order items for the RelatedList
52
+ const { tableProps } = useTable({
53
+ resource: "OrderItem",
54
+ filters: {
55
+ permanent: [
56
+ { field: "Order/Oid", operator: "eq", value: orderId || "" }
57
+ ]
58
+ },
59
+ syncWithLocation: false,
60
+ queryOptions: {
61
+ enabled: !!orderId,
62
+ },
63
+ pagination: {
64
+ mode: "off"
65
+ }
66
+ });
67
+
68
+ // Convert readonly array to mutable array for RelatedList
69
+ const dataSource = tableProps.dataSource ? [...tableProps.dataSource] : [];
70
+
71
+ return (
72
+ <Edit saveButtonProps={saveButtonProps}>
73
+ <Form {...formProps} layout="vertical">
74
+ <Form.Item
75
+ label={t("order.orderNumber")}
76
+ name="OrderNumber"
77
+ rules={[{ required: true, message: t("validation.required") }]}
78
+ >
79
+ <Input />
80
+ </Form.Item>
81
+ <Form.Item
82
+ label={t("order.orderDate")}
83
+ name="OrderDate"
84
+ getValueProps={(value) => ({
85
+ value: value ? dayjs(value) : null,
86
+ })}
87
+ getValueFromEvent={(date) => date?.toISOString()}
88
+ rules={[{ required: true, message: t("validation.required") }]}
89
+ >
90
+ <DatePicker style={{ width: "100%" }} />
91
+ </Form.Item>
92
+ <Form.Item
93
+ label={t("order.customerName")}
94
+ name="CustomerName"
95
+ rules={[{ required: true, message: t("validation.required") }]}
96
+ >
97
+ <Input />
98
+ </Form.Item>
99
+ <Form.Item
100
+ label={t("order.notes")}
101
+ name="Notes"
102
+ >
103
+ <Input.TextArea rows={3} />
104
+ </Form.Item>
105
+ </Form>
106
+
107
+ {/* Order Items (Details) */}
108
+ <Card title={t("order.items")} style={{ marginTop: 16 }}>
109
+ <RelatedList
110
+ resource="OrderItem"
111
+ masterField="Order"
112
+ masterId={orderId}
113
+ dataSource={dataSource}
114
+ FormFields={OrderItemFormFields}
115
+ modalTitle={t("orderItem.title")}
116
+ >
117
+ <Table.Column
118
+ dataIndex="ProductName"
119
+ title={t("orderItem.productName")}
120
+ />
121
+ <Table.Column
122
+ dataIndex="Quantity"
123
+ title={t("orderItem.quantity")}
124
+ />
125
+ <Table.Column
126
+ dataIndex="UnitPrice"
127
+ title={t("orderItem.unitPrice")}
128
+ render={(value: number) => value ? `$${value.toFixed(2)}` : "-"}
129
+ />
130
+ <Table.Column
131
+ dataIndex="Subtotal"
132
+ title={t("orderItem.subtotal")}
133
+ render={(value: number) => value ? `$${value.toFixed(2)}` : "-"}
134
+ />
135
+ </RelatedList>
136
+ </Card>
137
+ </Edit>
138
+ );
139
+ };
@@ -0,0 +1,4 @@
1
+ export { OrderList } from "./list";
2
+ export { OrderCreate } from "./create";
3
+ export { OrderEdit } from "./edit";
4
+ export { OrderShow } from "./show";
@@ -0,0 +1,59 @@
1
+ import React from "react";
2
+ import { BaseRecord } from "@refinedev/core";
3
+ import { EditButton, ShowButton, DeleteButton } from "@refinedev/antd";
4
+ import { Table, Space, Tag } from "antd";
5
+ import { SmartList } from "@cundi/refine-xaf";
6
+ import { useTranslation } from "react-i18next";
7
+
8
+ export const OrderList: React.FC = () => {
9
+ const { t } = useTranslation();
10
+
11
+ return (
12
+ <SmartList
13
+ resource="Order"
14
+ searchFields={["OrderNumber", "CustomerName"]}
15
+ >
16
+ <Table.Column
17
+ dataIndex="OrderNumber"
18
+ title={t("order.orderNumber")}
19
+ sorter
20
+ // @ts-ignore
21
+ defaultVisible
22
+ />
23
+ <Table.Column
24
+ dataIndex="OrderDate"
25
+ title={t("order.orderDate")}
26
+ sorter
27
+ render={(value: string) => value ? new Date(value).toLocaleDateString() : "-"}
28
+ // @ts-ignore
29
+ defaultVisible
30
+ />
31
+ <Table.Column
32
+ dataIndex="CustomerName"
33
+ title={t("order.customerName")}
34
+ sorter
35
+ // @ts-ignore
36
+ defaultVisible
37
+ />
38
+ <Table.Column
39
+ dataIndex="TotalAmount"
40
+ title={t("order.totalAmount")}
41
+ sorter
42
+ render={(value: number) => value ? `$${value.toFixed(2)}` : "$0.00"}
43
+ // @ts-ignore
44
+ defaultVisible
45
+ />
46
+ <Table.Column
47
+ title={t("buttons.list")}
48
+ dataIndex="actions"
49
+ render={(_, record: BaseRecord) => (
50
+ <Space>
51
+ <EditButton hideText size="small" recordItemId={record.Oid} />
52
+ <ShowButton hideText size="small" recordItemId={record.Oid} />
53
+ <DeleteButton hideText size="small" recordItemId={record.Oid} />
54
+ </Space>
55
+ )}
56
+ />
57
+ </SmartList>
58
+ );
59
+ };
@@ -0,0 +1,69 @@
1
+ import React from "react";
2
+ import { Show } from "@refinedev/antd";
3
+ import { useShow } from "@refinedev/core";
4
+ import { Typography, Table, Card, Descriptions } from "antd";
5
+ import { useTranslation } from "react-i18next";
6
+
7
+ const { Title } = Typography;
8
+
9
+ export const OrderShow: React.FC = () => {
10
+ const { t } = useTranslation();
11
+ const { query } = useShow({
12
+ meta: {
13
+ expand: ["Items"],
14
+ },
15
+ });
16
+ const { data, isLoading } = query;
17
+ const record = data?.data;
18
+
19
+ return (
20
+ <Show isLoading={isLoading}>
21
+ <Descriptions bordered column={1}>
22
+ <Descriptions.Item label={t("order.orderNumber")}>
23
+ {record?.OrderNumber}
24
+ </Descriptions.Item>
25
+ <Descriptions.Item label={t("order.orderDate")}>
26
+ {record?.OrderDate ? new Date(record.OrderDate).toLocaleDateString() : "-"}
27
+ </Descriptions.Item>
28
+ <Descriptions.Item label={t("order.customerName")}>
29
+ {record?.CustomerName}
30
+ </Descriptions.Item>
31
+ <Descriptions.Item label={t("order.totalAmount")}>
32
+ ${record?.TotalAmount?.toFixed(2) || "0.00"}
33
+ </Descriptions.Item>
34
+ <Descriptions.Item label={t("order.notes")}>
35
+ {record?.Notes || "-"}
36
+ </Descriptions.Item>
37
+ </Descriptions>
38
+
39
+ <Card title={t("order.items")} style={{ marginTop: 16 }}>
40
+ <Table
41
+ dataSource={record?.Items || []}
42
+ rowKey="Oid"
43
+ pagination={false}
44
+ bordered
45
+ size="small"
46
+ >
47
+ <Table.Column
48
+ dataIndex="ProductName"
49
+ title={t("orderItem.productName")}
50
+ />
51
+ <Table.Column
52
+ dataIndex="Quantity"
53
+ title={t("orderItem.quantity")}
54
+ />
55
+ <Table.Column
56
+ dataIndex="UnitPrice"
57
+ title={t("orderItem.unitPrice")}
58
+ render={(value: number) => value ? `$${value.toFixed(2)}` : "-"}
59
+ />
60
+ <Table.Column
61
+ dataIndex="Subtotal"
62
+ title={t("orderItem.subtotal")}
63
+ render={(value: number) => value ? `$${value.toFixed(2)}` : "-"}
64
+ />
65
+ </Table>
66
+ </Card>
67
+ </Show>
68
+ );
69
+ };