Compare commits
8 Commits
4f2bf864e4
...
production
| Author | SHA1 | Date | |
|---|---|---|---|
| e283d4a5c4 | |||
| f30ee4f6e3 | |||
| b9fc6ab247 | |||
| 7ad840fbda | |||
| 6dea561b05 | |||
| b73f00299b | |||
| a6593ed0e8 | |||
| 9f8877daf9 |
@@ -2,7 +2,7 @@ VITE_TOKEN_NAME = 'dmnu_a_t'
|
|||||||
VITE_REFRESH_TOKEN_NAME = 'dmnu-a-rt'
|
VITE_REFRESH_TOKEN_NAME = 'dmnu-a-rt'
|
||||||
|
|
||||||
VITE_BASE_URL = 'https://dmenu-api.danakcorp.com'
|
VITE_BASE_URL = 'https://dmenu-api.danakcorp.com'
|
||||||
#VITE_BASE_URL = 'http://192.168.1.107:2000'
|
# VITE_BASE_URL = 'http://192.168.99.131:2000'
|
||||||
|
|
||||||
VITE_SOCKET_URL = 'https://dmenu-api.danakcorp.com'
|
VITE_SOCKET_URL = 'https://dmenu-api.danakcorp.com'
|
||||||
VITE_INVOICE_URL = 'https://console.danakcorp.com/receipts/'
|
VITE_INVOICE_URL = 'https://console.danakcorp.com/receipts/'
|
||||||
Generated
+21
@@ -20,6 +20,7 @@
|
|||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"moment-jalaali": "^0.10.4",
|
"moment-jalaali": "^0.10.4",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"rc-rate": "^2.13.1",
|
"rc-rate": "^2.13.1",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"@types/leaflet": "^1.9.21",
|
"@types/leaflet": "^1.9.21",
|
||||||
"@types/moment-jalaali": "^0.7.9",
|
"@types/moment-jalaali": "^0.7.9",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/qrcode.react": "^1.0.5",
|
||||||
"@types/react": "^19.1.10",
|
"@types/react": "^19.1.10",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.7",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
@@ -2383,6 +2385,16 @@
|
|||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/qrcode.react": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/qrcode.react/-/qrcode.react-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-BghPtnlwvrvq8QkGa1H25YnN+5OIgCKFuQruncGWLGJYOzeSKiix/4+B9BtfKF2wf5ja8yfyWYA3OXju995G8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.1.11",
|
"version": "19.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.11.tgz",
|
||||||
@@ -4987,6 +4999,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode.react": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"moment-jalaali": "^0.10.4",
|
"moment-jalaali": "^0.10.4",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"rc-rate": "^2.13.1",
|
"rc-rate": "^2.13.1",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
"@types/leaflet": "^1.9.21",
|
"@types/leaflet": "^1.9.21",
|
||||||
"@types/moment-jalaali": "^0.7.9",
|
"@types/moment-jalaali": "^0.7.9",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/qrcode.react": "^1.0.5",
|
||||||
"@types/react": "^19.1.10",
|
"@types/react": "^19.1.10",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.7",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
|||||||
Binary file not shown.
@@ -6,7 +6,7 @@ import { NotificationSubject } from '@/types/notification.types'
|
|||||||
import NotificationItem from '@/components/NotificationItem'
|
import NotificationItem from '@/components/NotificationItem'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
const NOTIFICATION_SOUND_SRC = '/mixkit-correct-answer-tone-2870.wav'
|
const NOTIFICATION_SOUND_SRC = '/Qoddusi_improved.mp3'
|
||||||
|
|
||||||
interface NotificationItemData extends NotificationPayload {
|
interface NotificationItemData extends NotificationPayload {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
@@ -100,4 +100,7 @@ export const Pages = {
|
|||||||
wallet: {
|
wallet: {
|
||||||
transactions: "/wallet/transactions",
|
transactions: "/wallet/transactions",
|
||||||
},
|
},
|
||||||
|
aiChats: {
|
||||||
|
list: "/ai-chats/list",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+2
-1
@@ -69,7 +69,8 @@
|
|||||||
"pagers": "پیجرها",
|
"pagers": "پیجرها",
|
||||||
"notifications": "تنظیمات اعلانات",
|
"notifications": "تنظیمات اعلانات",
|
||||||
"statistics": "گزارشات",
|
"statistics": "گزارشات",
|
||||||
"sliders": "اسلایدرها"
|
"sliders": "اسلایدرها",
|
||||||
|
"ai_chats": "چتهای AI"
|
||||||
},
|
},
|
||||||
"shipment": {
|
"shipment": {
|
||||||
"dineIn": "سرو در رستوران",
|
"dineIn": "سرو در رستوران",
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { type FC, useMemo, useState } from "react";
|
||||||
|
import Table from "@/components/Table";
|
||||||
|
import Filters from "@/components/Filters";
|
||||||
|
import DefaulModal from "@/components/DefaulModal";
|
||||||
|
import { useGetAiChats } from "./hooks/useAiChatData";
|
||||||
|
import { useAiChatFilters } from "./hooks/useAiChatFilters";
|
||||||
|
import { getAiChatTableColumns } from "./components/AiChatTableColumns";
|
||||||
|
import { useAiChatFiltersFields } from "./components/AiChatFiltersFields";
|
||||||
|
import {
|
||||||
|
getAiChatAnswer,
|
||||||
|
getAiChatQuestion,
|
||||||
|
type AiChat,
|
||||||
|
} from "./types/Types";
|
||||||
|
import type { RowDataType } from "@/components/types/TableTypes";
|
||||||
|
import { formatPrice } from "@/helpers/func";
|
||||||
|
|
||||||
|
const AiChatsList: FC = () => {
|
||||||
|
const [selectedChat, setSelectedChat] = useState<AiChat | null>(null);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
filters,
|
||||||
|
currentPage,
|
||||||
|
apiParams,
|
||||||
|
handleFiltersChange,
|
||||||
|
handlePageChange,
|
||||||
|
} = useAiChatFilters();
|
||||||
|
|
||||||
|
const { data: chatsData, isLoading } = useGetAiChats(apiParams);
|
||||||
|
|
||||||
|
const chats = chatsData?.data || [];
|
||||||
|
const totalPages = chatsData?.meta?.totalPages || 1;
|
||||||
|
|
||||||
|
const filterFields = useAiChatFiltersFields();
|
||||||
|
const columns = useMemo(
|
||||||
|
() =>
|
||||||
|
getAiChatTableColumns({
|
||||||
|
onViewAnswer: (chat) => {
|
||||||
|
setSelectedChat(chat);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
setSelectedChat(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-5">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h1 className="text-lg font-light">لیست چتهای AI</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<Filters
|
||||||
|
fields={filterFields}
|
||||||
|
onChange={handleFiltersChange}
|
||||||
|
initialValues={filters}
|
||||||
|
searchField="search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table<AiChat & RowDataType>
|
||||||
|
columns={columns}
|
||||||
|
data={chats as (AiChat & RowDataType)[]}
|
||||||
|
isloading={isLoading}
|
||||||
|
pagination={{
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
onPageChange: handlePageChange,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DefaulModal
|
||||||
|
open={isModalOpen}
|
||||||
|
close={handleCloseModal}
|
||||||
|
isHeader={true}
|
||||||
|
title_header="جزئیات چت"
|
||||||
|
>
|
||||||
|
<div className="mt-4 space-y-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-description mb-1">سوال</div>
|
||||||
|
<p className="text-sm text-foreground whitespace-pre-wrap break-words">
|
||||||
|
{(selectedChat && getAiChatQuestion(selectedChat)) || "-"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-description mb-1">پاسخ</div>
|
||||||
|
<p className="text-sm text-foreground whitespace-pre-wrap break-words">
|
||||||
|
{(selectedChat && getAiChatAnswer(selectedChat)) || "-"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{typeof selectedChat?.cost === "number" && (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-description mb-1">هزینه</div>
|
||||||
|
<p className="text-sm text-foreground">
|
||||||
|
{formatPrice(selectedChat.cost)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DefaulModal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AiChatsList;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import type { FieldType } from "@/components/Filters";
|
||||||
|
|
||||||
|
export const useAiChatFiltersFields = (): FieldType[] => {
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
name: "search",
|
||||||
|
placeholder: "جستجو در سوالات",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { Eye } from "iconsax-react";
|
||||||
|
import type { ColumnType } from "@/components/types/TableTypes";
|
||||||
|
import {
|
||||||
|
getAiChatAnswer,
|
||||||
|
getAiChatQuestion,
|
||||||
|
getAiChatTokens,
|
||||||
|
type AiChat,
|
||||||
|
} from "../types/Types";
|
||||||
|
import { formatOptionalDate, formatPrice } from "@/helpers/func";
|
||||||
|
|
||||||
|
interface GetAiChatTableColumnsProps {
|
||||||
|
onViewAnswer?: (chat: AiChat) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAiChatTableColumns = (
|
||||||
|
props?: GetAiChatTableColumnsProps
|
||||||
|
): ColumnType<AiChat>[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: "question",
|
||||||
|
title: "سوال",
|
||||||
|
render: (item: AiChat) => {
|
||||||
|
const question = getAiChatQuestion(item);
|
||||||
|
return (
|
||||||
|
<div className="max-w-md truncate" title={question}>
|
||||||
|
{question || "-"}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "type",
|
||||||
|
title: "نوع",
|
||||||
|
render: (item: AiChat) => {
|
||||||
|
return item.type || "-";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "cost",
|
||||||
|
title: "هزینه مصرفشده",
|
||||||
|
render: (item: AiChat) => {
|
||||||
|
return typeof item.cost === "number" ? formatPrice(item.cost) : "-";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "consumedTokens",
|
||||||
|
title: "توکن مصرفی",
|
||||||
|
render: (item: AiChat) => {
|
||||||
|
const tokens = getAiChatTokens(item);
|
||||||
|
return typeof tokens === "number"
|
||||||
|
? tokens.toLocaleString("fa-IR")
|
||||||
|
: "-";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "createdAt",
|
||||||
|
title: "تاریخ",
|
||||||
|
render: (item: AiChat) => {
|
||||||
|
return formatOptionalDate(item.createdAt, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "details",
|
||||||
|
title: "",
|
||||||
|
render: (item: AiChat) => {
|
||||||
|
if (!getAiChatAnswer(item) || !props?.onViewAnswer) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Eye
|
||||||
|
size={20}
|
||||||
|
color="#8C90A3"
|
||||||
|
className="cursor-pointer hover:opacity-70 transition-opacity"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
props.onViewAnswer?.(item);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import * as api from "../service/AiChatService";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import type { GetAiChatsParams } from "../types/Types";
|
||||||
|
|
||||||
|
export const useGetAiChats = (params?: GetAiChatsParams) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["ai-chats", params],
|
||||||
|
queryFn: () => api.getAiChats(params),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import type { FilterValues } from "@/components/Filters";
|
||||||
|
import type { GetAiChatsParams } from "../types/Types";
|
||||||
|
|
||||||
|
const DEFAULT_PAGE = 1;
|
||||||
|
const DEFAULT_LIMIT = 10;
|
||||||
|
|
||||||
|
export const useAiChatFilters = () => {
|
||||||
|
const [filters, setFilters] = useState<FilterValues>({});
|
||||||
|
const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE);
|
||||||
|
const [limit] = useState(DEFAULT_LIMIT);
|
||||||
|
|
||||||
|
const apiParams = useMemo<GetAiChatsParams>(() => {
|
||||||
|
const params: GetAiChatsParams = {
|
||||||
|
page: currentPage,
|
||||||
|
limit,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (filters.search) {
|
||||||
|
params.search = filters.search as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}, [filters, currentPage, limit]);
|
||||||
|
|
||||||
|
const handleFiltersChange = (newFilters: FilterValues) => {
|
||||||
|
setFilters(newFilters);
|
||||||
|
setCurrentPage(DEFAULT_PAGE);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
filters,
|
||||||
|
currentPage,
|
||||||
|
apiParams,
|
||||||
|
handleFiltersChange,
|
||||||
|
handlePageChange,
|
||||||
|
limit,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import axios from "@/config/axios";
|
||||||
|
import type { GetAiChatsParams, GetAiChatsResponse } from "../types/Types";
|
||||||
|
|
||||||
|
export const getAiChats = async (
|
||||||
|
params?: GetAiChatsParams
|
||||||
|
): Promise<GetAiChatsResponse> => {
|
||||||
|
const { data } = await axios.get<GetAiChatsResponse>("/admin/ai/chats", {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { IResponse } from "@/types/response.types";
|
||||||
|
import type { RowDataType } from "@/components/types/TableTypes";
|
||||||
|
|
||||||
|
export interface AiChat extends RowDataType {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
deletedAt?: string | null;
|
||||||
|
question?: string;
|
||||||
|
prompt?: string;
|
||||||
|
message?: string;
|
||||||
|
answer?: string | null;
|
||||||
|
response?: string | null;
|
||||||
|
cost?: number;
|
||||||
|
consumedTokens?: number;
|
||||||
|
totalTokens?: number;
|
||||||
|
promptTokens?: number;
|
||||||
|
completionTokens?: number;
|
||||||
|
type?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaginationMeta = {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
totalCost?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetAiChatsParams = {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetAiChatsResponse = IResponse<AiChat[]> & {
|
||||||
|
meta: PaginationMeta;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAiChatQuestion = (chat: AiChat): string =>
|
||||||
|
chat.question || chat.prompt || chat.message || "";
|
||||||
|
|
||||||
|
export const getAiChatAnswer = (chat: AiChat): string =>
|
||||||
|
chat.answer || chat.response || "";
|
||||||
|
|
||||||
|
export const getAiChatTokens = (chat: AiChat): number | undefined =>
|
||||||
|
chat.consumedTokens ?? chat.totalTokens ?? chat.promptTokens;
|
||||||
+6
-159
@@ -1,8 +1,4 @@
|
|||||||
import Button from "@/components/Button";
|
import Button from "@/components/Button";
|
||||||
import Input from "@/components/Input";
|
|
||||||
import Select from "@/components/Select";
|
|
||||||
import Textarea from "@/components/Textarea";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import type { ErrorType } from "@/helpers/types";
|
import type { ErrorType } from "@/helpers/types";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { TickCircle } from "iconsax-react";
|
import { TickCircle } from "iconsax-react";
|
||||||
@@ -11,8 +7,11 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import { useMultipleUpload } from "../uploader/hooks/useUploaderData";
|
import { useMultipleUpload } from "../uploader/hooks/useUploaderData";
|
||||||
|
import BasicInfoSection from "./components/BasicInfoSection";
|
||||||
import CreateFoodSidebar from "./components/CreateFoodSidebar";
|
import CreateFoodSidebar from "./components/CreateFoodSidebar";
|
||||||
|
import FoodContentSection from "./components/FoodContentSection";
|
||||||
import MealTimesSection from "./components/MealTimesSection";
|
import MealTimesSection from "./components/MealTimesSection";
|
||||||
|
import ServiceOptionsSection from "./components/ServiceOptionsSection";
|
||||||
import WeekDaysSection from "./components/WeekDaysSection";
|
import WeekDaysSection from "./components/WeekDaysSection";
|
||||||
import { useCreateFood, useGetCategories } from "./hooks/useFoodData";
|
import { useCreateFood, useGetCategories } from "./hooks/useFoodData";
|
||||||
import type { CreateFoodType } from "./types/Types";
|
import type { CreateFoodType } from "./types/Types";
|
||||||
@@ -117,163 +116,11 @@ const CreateFood: FC = () => {
|
|||||||
<div className="flex flex-col lg:flex-row gap-6 mt-6">
|
<div className="flex flex-col lg:flex-row gap-6 mt-6">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="bg-white py-6 sm:py-8 xl:px-10 px-4 rounded-3xl">
|
<div className="bg-white py-6 sm:py-8 xl:px-10 px-4 rounded-3xl">
|
||||||
<Input
|
<BasicInfoSection formik={formik} categories={categories} />
|
||||||
label="نام غذا"
|
<FoodContentSection formik={formik} />
|
||||||
name="title"
|
|
||||||
placeholder=""
|
|
||||||
value={formik.values.title}
|
|
||||||
onChange={formik.handleChange}
|
|
||||||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ""}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="mt-5">
|
|
||||||
<Select
|
|
||||||
items={categories}
|
|
||||||
label="دسته بندی"
|
|
||||||
placeholder="انتخاب کنید"
|
|
||||||
name="categoryId"
|
|
||||||
value={formik.values.categoryId}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
formik.setFieldValue("categoryId", value);
|
|
||||||
}}
|
|
||||||
error_text={formik.touched.categoryId && formik.errors.categoryId ? String(formik.errors.categoryId) : ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 mt-5">
|
|
||||||
<Input
|
|
||||||
name="price"
|
|
||||||
label="قیمت"
|
|
||||||
placeholder="تومان"
|
|
||||||
type="number"
|
|
||||||
seprator={true}
|
|
||||||
value={formik.values.price}
|
|
||||||
onChange={(e) => formik.setFieldValue("price", Number(e.target.value))}
|
|
||||||
error_text={formik.touched.price && formik.errors.price ? formik.errors.price : ""}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
name="discount"
|
|
||||||
label="تخفیف"
|
|
||||||
placeholder="تومان"
|
|
||||||
type="number"
|
|
||||||
seprator={true}
|
|
||||||
value={formik.values.discount}
|
|
||||||
onChange={(e) => formik.setFieldValue("discount", Number(e.target.value))}
|
|
||||||
error_text={formik.touched.discount && formik.errors.discount ? formik.errors.discount : ""}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
name="prepareTime"
|
|
||||||
label="زمان آماده سازی"
|
|
||||||
placeholder="دقیقه"
|
|
||||||
type="number"
|
|
||||||
value={formik.values.prepareTime}
|
|
||||||
onChange={(e) => formik.setFieldValue("prepareTime", Number(e.target.value))}
|
|
||||||
error_text={formik.touched.prepareTime && formik.errors.prepareTime ? formik.errors.prepareTime : ""}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
name="dailyStock"
|
|
||||||
label="موجودی روزانه"
|
|
||||||
placeholder="عدد"
|
|
||||||
type="number"
|
|
||||||
seprator={true}
|
|
||||||
value={formik.values.dailyStock}
|
|
||||||
onChange={(e) => formik.setFieldValue("dailyStock", Number(e.target.value))}
|
|
||||||
error_text={formik.touched.dailyStock && formik.errors.dailyStock ? formik.errors.dailyStock : ""}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
name="availableStock"
|
|
||||||
label="موجودی فعلی"
|
|
||||||
placeholder="عدد"
|
|
||||||
type="number"
|
|
||||||
seprator={true}
|
|
||||||
value={formik.values.availableStock}
|
|
||||||
onChange={(e) => formik.setFieldValue("availableStock", Number(e.target.value))}
|
|
||||||
error_text={formik.touched.availableStock && formik.errors.availableStock ? formik.errors.availableStock : ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5">
|
|
||||||
<Textarea
|
|
||||||
name="desc"
|
|
||||||
label="توضیحات غذا"
|
|
||||||
placeholder=""
|
|
||||||
value={formik.values.desc}
|
|
||||||
onChange={formik.handleChange}
|
|
||||||
error_text={formik.touched.desc && formik.errors.desc ? formik.errors.desc : ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5">
|
|
||||||
<Input
|
|
||||||
name="order"
|
|
||||||
label="اولویت"
|
|
||||||
placeholder="عدد"
|
|
||||||
type="number"
|
|
||||||
value={formik.values.order}
|
|
||||||
onChange={(e) => formik.setFieldValue("order", Number(e.target.value))}
|
|
||||||
error_text={formik.touched.order && formik.errors.order ? formik.errors.order : ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5">
|
|
||||||
<div className="text-sm mb-2">محتوای غذا</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{formik.values.content.map((item, index) => (
|
|
||||||
<div key={index} className="flex gap-2 items-center">
|
|
||||||
<Input
|
|
||||||
placeholder="متن محتوا"
|
|
||||||
value={item}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newContent = [...formik.values.content];
|
|
||||||
newContent[index] = e.target.value;
|
|
||||||
formik.setFieldValue("content", newContent);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="px-3 bg-red-500 hover:bg-red-600 w-auto"
|
|
||||||
onClick={() => {
|
|
||||||
const newContent = formik.values.content.filter((_, i) => i !== index);
|
|
||||||
formik.setFieldValue("content", newContent);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
حذف
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="w-full bg-gray-200 text-gray-700 hover:bg-gray-300"
|
|
||||||
onClick={() => {
|
|
||||||
formik.setFieldValue("content", [...formik.values.content, ""]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
افزودن محتوا
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MealTimesSection formik={formik} />
|
<MealTimesSection formik={formik} />
|
||||||
<WeekDaysSection formik={formik} />
|
<WeekDaysSection formik={formik} />
|
||||||
|
<ServiceOptionsSection formik={formik} />
|
||||||
<div className="mt-6">
|
|
||||||
<div className="text-sm mb-3">گزینههای سرویس</div>
|
|
||||||
<div className="flex gap-6 flex-wrap">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox checked={formik.values.pickupServe} onCheckedChange={(checked) => formik.setFieldValue("pickupServe", checked)} />
|
|
||||||
<label className="text-xs cursor-pointer"> بیرونبر</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox checked={formik.values.inPlaceServe} onCheckedChange={(checked) => formik.setFieldValue("inPlaceServe", checked)} />
|
|
||||||
<label className="text-xs cursor-pointer">سرو در محل</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,53 @@
|
|||||||
import { type FC } from 'react'
|
import { type FC } from 'react'
|
||||||
import type { FormikProps } from 'formik'
|
import type { FormikProps } from 'formik'
|
||||||
|
import { Magicpen } from 'iconsax-react'
|
||||||
import Input from '@/components/Input'
|
import Input from '@/components/Input'
|
||||||
import Select from '@/components/Select'
|
import Select from '@/components/Select'
|
||||||
import Textarea from '@/components/Textarea'
|
import Textarea from '@/components/Textarea'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { extractErrorMessage } from '@/config/func'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
import type { CreateFoodType } from '../types/Types'
|
import type { CreateFoodType } from '../types/Types'
|
||||||
|
import { useGenerateFoodDescription } from '../hooks/useFoodData'
|
||||||
|
|
||||||
type BasicInfoSectionProps = {
|
type BasicInfoSectionProps = {
|
||||||
formik: FormikProps<CreateFoodType>
|
formik: FormikProps<CreateFoodType>
|
||||||
categories: Array<{ label: string; value: string }>
|
categories: Array<{ label: string; value: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseDescription = (data: { answer?: string } | undefined): string => {
|
||||||
|
return data?.answer?.trim() || ''
|
||||||
|
}
|
||||||
|
|
||||||
const BasicInfoSection: FC<BasicInfoSectionProps> = ({ formik, categories }) => {
|
const BasicInfoSection: FC<BasicInfoSectionProps> = ({ formik, categories }) => {
|
||||||
|
const { mutate: generateDescription, isPending: isGeneratingDesc } = useGenerateFoodDescription()
|
||||||
|
|
||||||
|
const handleGenerateDescription = () => {
|
||||||
|
const foodName = formik.values.title.trim()
|
||||||
|
if (!foodName) {
|
||||||
|
toast.error('لطفاً ابتدا نام کامل غذا را وارد کنید')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
generateDescription(
|
||||||
|
{ foodName },
|
||||||
|
{
|
||||||
|
onSuccess: (response) => {
|
||||||
|
const description = parseDescription(response.data)
|
||||||
|
if (!description) {
|
||||||
|
toast.error('توضیحی از هوش مصنوعی دریافت نشد')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
formik.setFieldValue('desc', description)
|
||||||
|
toast.success('توضیحات با موفقیت تولید شد')
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
@@ -94,9 +131,22 @@ const BasicInfoSection: FC<BasicInfoSectionProps> = ({ formik, categories }) =>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
|
<div className='flex items-center justify-between mb-1'>
|
||||||
|
<div className='text-sm'>توضیحات غذا</div>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
className='w-auto h-8 px-3 bg-transparent text-primary border border-primary text-xs gap-1.5'
|
||||||
|
onClick={handleGenerateDescription}
|
||||||
|
isloading={isGeneratingDesc}
|
||||||
|
colorLoading='black'
|
||||||
|
disabled={!formik.values.title.trim() || isGeneratingDesc}
|
||||||
|
>
|
||||||
|
<Magicpen size={16} color='currentColor' />
|
||||||
|
تولید با AI
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<Textarea
|
<Textarea
|
||||||
name='desc'
|
name='desc'
|
||||||
label='توضیحات غذا'
|
|
||||||
placeholder=''
|
placeholder=''
|
||||||
value={formik.values.desc}
|
value={formik.values.desc}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
@@ -120,4 +170,3 @@ const BasicInfoSection: FC<BasicInfoSectionProps> = ({ formik, categories }) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default BasicInfoSection
|
export default BasicInfoSection
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,70 @@
|
|||||||
import { type FC } from 'react'
|
import { type FC } from 'react'
|
||||||
import type { FormikProps } from 'formik'
|
import type { FormikProps } from 'formik'
|
||||||
|
import { Magicpen } from 'iconsax-react'
|
||||||
import Input from '@/components/Input'
|
import Input from '@/components/Input'
|
||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
|
import { extractErrorMessage } from '@/config/func'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
import type { CreateFoodType } from '../types/Types'
|
import type { CreateFoodType } from '../types/Types'
|
||||||
|
import { useGenerateFoodContent } from '../hooks/useFoodData'
|
||||||
|
|
||||||
type FoodContentSectionProps = {
|
type FoodContentSectionProps = {
|
||||||
formik: FormikProps<CreateFoodType>
|
formik: FormikProps<CreateFoodType>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseContent = (data: { answer?: string } | undefined): string[] => {
|
||||||
|
if (!data?.answer) return []
|
||||||
|
return data.answer
|
||||||
|
.split('،')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
const FoodContentSection: FC<FoodContentSectionProps> = ({ formik }) => {
|
const FoodContentSection: FC<FoodContentSectionProps> = ({ formik }) => {
|
||||||
|
const { mutate: generateContent, isPending: isGeneratingContent } = useGenerateFoodContent()
|
||||||
|
|
||||||
|
const handleGenerateContent = () => {
|
||||||
|
const foodName = formik.values.title.trim()
|
||||||
|
if (!foodName) {
|
||||||
|
toast.error('لطفاً ابتدا نام کامل غذا را وارد کنید')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
generateContent(
|
||||||
|
{ foodName },
|
||||||
|
{
|
||||||
|
onSuccess: (response) => {
|
||||||
|
const content = parseContent(response.data)
|
||||||
|
if (!content.length) {
|
||||||
|
toast.error('محتوایی از هوش مصنوعی دریافت نشد')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
formik.setFieldValue('content', content)
|
||||||
|
toast.success('محتوای غذا با موفقیت تولید شد')
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<div className='text-sm mb-2'>محتوای غذا</div>
|
<div className='flex items-center justify-between mb-2'>
|
||||||
|
<div className='text-sm'>محتوای غذا</div>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
className='w-auto h-8 px-3 bg-transparent text-primary border border-primary text-xs gap-1.5'
|
||||||
|
onClick={handleGenerateContent}
|
||||||
|
isloading={isGeneratingContent}
|
||||||
|
colorLoading='black'
|
||||||
|
disabled={!formik.values.title.trim() || isGeneratingContent}
|
||||||
|
>
|
||||||
|
<Magicpen size={16} color='currentColor' />
|
||||||
|
تولید با AI
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
{formik.values.content.map((item, index) => (
|
{formik.values.content.map((item, index) => (
|
||||||
<div key={index} className='flex gap-2 items-center'>
|
<div key={index} className='flex gap-2 items-center'>
|
||||||
|
|||||||
@@ -113,3 +113,15 @@ export const useSetStockForFood = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGenerateFoodDescription = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.generateFoodDescription,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGenerateFoodContent = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.generateFoodContent,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import axios from "@/config/axios";
|
|||||||
import type {
|
import type {
|
||||||
CreateCategoryType,
|
CreateCategoryType,
|
||||||
CreateFoodType,
|
CreateFoodType,
|
||||||
|
GenerateFoodAiParams,
|
||||||
|
GenerateFoodContentResponse,
|
||||||
|
GenerateFoodDescriptionResponse,
|
||||||
GetCategoriesResponseType,
|
GetCategoriesResponseType,
|
||||||
GetFoodDetailsResponseType,
|
GetFoodDetailsResponseType,
|
||||||
GetFoodsParams,
|
GetFoodsParams,
|
||||||
@@ -90,3 +93,23 @@ export const setStockForFood = async (foodId: string, params: SetStockType) => {
|
|||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateFoodDescription = async (
|
||||||
|
params: GenerateFoodAiParams,
|
||||||
|
): Promise<GenerateFoodDescriptionResponse> => {
|
||||||
|
const { data } = await axios.post<GenerateFoodDescriptionResponse>(
|
||||||
|
`/admin/ai/food-description`,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateFoodContent = async (
|
||||||
|
params: GenerateFoodAiParams,
|
||||||
|
): Promise<GenerateFoodContentResponse> => {
|
||||||
|
const { data } = await axios.post<GenerateFoodContentResponse>(
|
||||||
|
`/admin/ai/food-content`,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -155,3 +155,18 @@ export type SetStockType = {
|
|||||||
totalStock: number;
|
totalStock: number;
|
||||||
availableStock: number;
|
availableStock: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GenerateFoodAiParams = {
|
||||||
|
foodName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GenerateFoodAiResult = {
|
||||||
|
answer: string;
|
||||||
|
consumedTokens?: number;
|
||||||
|
cost?: number;
|
||||||
|
promptTokens?: number;
|
||||||
|
completionTokens?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GenerateFoodDescriptionResponse = IResponse<GenerateFoodAiResult>;
|
||||||
|
export type GenerateFoodContentResponse = IResponse<GenerateFoodAiResult>;
|
||||||
|
|||||||
@@ -9,12 +9,16 @@ import {
|
|||||||
useAddOrderItem,
|
useAddOrderItem,
|
||||||
useUpdateOrderItemQuantity,
|
useUpdateOrderItemQuantity,
|
||||||
useRemoveOrderItem,
|
useRemoveOrderItem,
|
||||||
|
useSendPleaseComeSms,
|
||||||
} from './hooks/useOrderData'
|
} from './hooks/useOrderData'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
|
import { ArrowRight2 } from 'iconsax-react'
|
||||||
import { formatFaNumber } from '@/helpers/func'
|
import { formatFaNumber } from '@/helpers/func'
|
||||||
|
import { Pages } from '@/config/Pages'
|
||||||
import { OrderStatus, PaymentStatusEnum } from './enum/Enum'
|
import { OrderStatus, PaymentStatusEnum } from './enum/Enum'
|
||||||
import { PaymentMethodEnum } from '@/pages/paymentMethods/enum/Enum'
|
import { PaymentMethodEnum } from '@/pages/paymentMethods/enum/Enum'
|
||||||
import { DeliveryMethodEnum } from '@/pages/shipmentMethod/enum/Enum'
|
import { DeliveryMethodEnum } from '@/pages/shipmentMethod/enum/Enum'
|
||||||
|
import Button from '@/components/Button'
|
||||||
import ModalConfrim from '@/components/ModalConfrim'
|
import ModalConfrim from '@/components/ModalConfrim'
|
||||||
import CustomerInfo from './components/CustomerInfo'
|
import CustomerInfo from './components/CustomerInfo'
|
||||||
import OrderItemsDetails from './components/OrderItemsDetails'
|
import OrderItemsDetails from './components/OrderItemsDetails'
|
||||||
@@ -25,6 +29,7 @@ import UpdateOrderFeesModal from './components/UpdateOrderFeesModal'
|
|||||||
import AddOrderItemModal from './components/AddOrderItemModal'
|
import AddOrderItemModal from './components/AddOrderItemModal'
|
||||||
import RefundPaymentModal from './components/RefundPaymentModal'
|
import RefundPaymentModal from './components/RefundPaymentModal'
|
||||||
import AddPaymentModal from './components/AddPaymentModal'
|
import AddPaymentModal from './components/AddPaymentModal'
|
||||||
|
import SendPleaseComeSmsModal from './components/SendPleaseComeSmsModal'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
import { printOrderReceipt } from './print/orderReceiptPrint'
|
import { printOrderReceipt } from './print/orderReceiptPrint'
|
||||||
@@ -32,6 +37,7 @@ import type { UpdateOrderFeesType } from './types/Types'
|
|||||||
|
|
||||||
const OrderDetails: FC = () => {
|
const OrderDetails: FC = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
|
const navigate = useNavigate()
|
||||||
const { data: orderData, isLoading } = useGetOrderById(id!)
|
const { data: orderData, isLoading } = useGetOrderById(id!)
|
||||||
const { mutate: verifyPayment, isPending: isVerifyingPayment } = useVerifyPayment()
|
const { mutate: verifyPayment, isPending: isVerifyingPayment } = useVerifyPayment()
|
||||||
const { mutate: changeOrderStatus, isPending: isChangingStatus } = useChangeOrderStatus()
|
const { mutate: changeOrderStatus, isPending: isChangingStatus } = useChangeOrderStatus()
|
||||||
@@ -41,8 +47,10 @@ const OrderDetails: FC = () => {
|
|||||||
const { mutate: addOrderItem, isPending: isAddingItem } = useAddOrderItem()
|
const { mutate: addOrderItem, isPending: isAddingItem } = useAddOrderItem()
|
||||||
const { mutate: updateOrderItemQuantity, isPending: isUpdatingItemQuantity } = useUpdateOrderItemQuantity()
|
const { mutate: updateOrderItemQuantity, isPending: isUpdatingItemQuantity } = useUpdateOrderItemQuantity()
|
||||||
const { mutate: removeOrderItem, isPending: isRemovingItem } = useRemoveOrderItem()
|
const { mutate: removeOrderItem, isPending: isRemovingItem } = useRemoveOrderItem()
|
||||||
|
const { mutate: sendPleaseComeSms, isPending: isSendingPleaseComeSms } = useSendPleaseComeSms()
|
||||||
|
|
||||||
const [showCancelModal, setShowCancelModal] = useState(false)
|
const [showCancelModal, setShowCancelModal] = useState(false)
|
||||||
|
const [showPleaseComeSmsModal, setShowPleaseComeSmsModal] = useState(false)
|
||||||
const [refundModal, setRefundModal] = useState<{ defaultAmount: number; maxAmount: number } | null>(null)
|
const [refundModal, setRefundModal] = useState<{ defaultAmount: number; maxAmount: number } | null>(null)
|
||||||
const [showAddPaymentModal, setShowAddPaymentModal] = useState(false)
|
const [showAddPaymentModal, setShowAddPaymentModal] = useState(false)
|
||||||
const [showEditFeesModal, setShowEditFeesModal] = useState(false)
|
const [showEditFeesModal, setShowEditFeesModal] = useState(false)
|
||||||
@@ -98,6 +106,7 @@ const OrderDetails: FC = () => {
|
|||||||
OrderStatus.DELIVERED_TO_RECEPTIONIST,
|
OrderStatus.DELIVERED_TO_RECEPTIONIST,
|
||||||
OrderStatus.SHIPPED,
|
OrderStatus.SHIPPED,
|
||||||
].includes(order.status as OrderStatus)
|
].includes(order.status as OrderStatus)
|
||||||
|
const canSendPleaseComeSms = Boolean(order.user?.phone)
|
||||||
const isCourierDelivery = order.deliveryMethod?.method === DeliveryMethodEnum.DeliveryCourier
|
const isCourierDelivery = order.deliveryMethod?.method === DeliveryMethodEnum.DeliveryCourier
|
||||||
const isDineInOrCarDelivery =
|
const isDineInOrCarDelivery =
|
||||||
order.deliveryMethod?.method === DeliveryMethodEnum.DineIn ||
|
order.deliveryMethod?.method === DeliveryMethodEnum.DineIn ||
|
||||||
@@ -306,12 +315,33 @@ const OrderDetails: FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleConfirmPleaseComeSms = () => {
|
||||||
|
sendPleaseComeSms(order.id, {
|
||||||
|
onSuccess: (response) => {
|
||||||
|
toast.success(response.message || 'پیامک در صف ارسال قرار گرفت')
|
||||||
|
setShowPleaseComeSmsModal(false)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-5 pb-8'>
|
<div className='mt-5 pb-8'>
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
<h1 className='text-lg font-light'>
|
<h1 className='text-lg font-light'>
|
||||||
سفارش {formatFaNumber(order.orderNumber)}
|
سفارش {formatFaNumber(order.orderNumber)}
|
||||||
</h1>
|
</h1>
|
||||||
|
<Button
|
||||||
|
className='w-fit px-6 bg-transparent text-primary border-primary border'
|
||||||
|
onClick={() => navigate(Pages.orders.list)}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2 items-center'>
|
||||||
|
<ArrowRight2 color='#000' size={18} />
|
||||||
|
<span>بازگشت</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col lg:flex-row gap-5 lg:gap-7 mt-6 sm:mt-9'>
|
<div className='flex flex-col lg:flex-row gap-5 lg:gap-7 mt-6 sm:mt-9'>
|
||||||
@@ -356,13 +386,16 @@ const OrderDetails: FC = () => {
|
|||||||
isCustomerPickup={isCustomerPickup}
|
isCustomerPickup={isCustomerPickup}
|
||||||
isCanceled={isCanceled}
|
isCanceled={isCanceled}
|
||||||
canCompleteOrder={canCompleteOrder}
|
canCompleteOrder={canCompleteOrder}
|
||||||
|
canSendPleaseComeSms={canSendPleaseComeSms}
|
||||||
isChangingStatus={isChangingStatus}
|
isChangingStatus={isChangingStatus}
|
||||||
|
isSendingPleaseComeSms={isSendingPleaseComeSms}
|
||||||
onSendToKitchen={handleSendToKitchen}
|
onSendToKitchen={handleSendToKitchen}
|
||||||
onDeliverToCourier={handleDeliverToCourier}
|
onDeliverToCourier={handleDeliverToCourier}
|
||||||
onDeliverToWaiter={handleDeliverToWaiter}
|
onDeliverToWaiter={handleDeliverToWaiter}
|
||||||
onDeliverToReceptionist={handleDeliverToReceptionist}
|
onDeliverToReceptionist={handleDeliverToReceptionist}
|
||||||
onCompleteOrder={handleCompleteOrder}
|
onCompleteOrder={handleCompleteOrder}
|
||||||
onCancelOrder={() => setShowCancelModal(true)}
|
onCancelOrder={() => setShowCancelModal(true)}
|
||||||
|
onSendPleaseComeSms={() => setShowPleaseComeSmsModal(true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -416,6 +449,15 @@ const OrderDetails: FC = () => {
|
|||||||
isloading={isRemovingItem}
|
isloading={isRemovingItem}
|
||||||
label='آیا از حذف این آیتم مطمئن هستید؟'
|
label='آیا از حذف این آیتم مطمئن هستید؟'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SendPleaseComeSmsModal
|
||||||
|
isOpen={showPleaseComeSmsModal}
|
||||||
|
onClose={() => setShowPleaseComeSmsModal(false)}
|
||||||
|
isLoading={isSendingPleaseComeSms}
|
||||||
|
customerName={order.user?.firstName || ''}
|
||||||
|
restaurantName={order.restaurant?.name || ''}
|
||||||
|
onConfirm={handleConfirmPleaseComeSms}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type FC } from 'react'
|
import { type FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Add } from 'iconsax-react'
|
import { Add } from 'iconsax-react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
@@ -6,12 +6,15 @@ import Table from '@/components/Table'
|
|||||||
import Filters from '@/components/Filters'
|
import Filters from '@/components/Filters'
|
||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import { Pages } from '@/config/Pages'
|
import { Pages } from '@/config/Pages'
|
||||||
import { useGetOrders } from './hooks/useOrderData'
|
import { useGetOrders, useSendPleaseComeSms } from './hooks/useOrderData'
|
||||||
import { useOrderFilters } from './hooks/useOrderFilters'
|
import { useOrderFilters } from './hooks/useOrderFilters'
|
||||||
import { getOrderTableColumns } from './components/OrderTableColumns'
|
import { getOrderTableColumns } from './components/OrderTableColumns'
|
||||||
import { useOrderFiltersFields } from './components/OrderFiltersFields'
|
import { useOrderFiltersFields } from './components/OrderFiltersFields'
|
||||||
|
import SendPleaseComeSmsModal from './components/SendPleaseComeSmsModal'
|
||||||
import type { Order } from './types/Types'
|
import type { Order } from './types/Types'
|
||||||
import type { RowDataType } from '@/components/types/TableTypes'
|
import type { RowDataType } from '@/components/types/TableTypes'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { extractErrorMessage } from '@/config/func'
|
||||||
|
|
||||||
const OrdersList: FC = () => {
|
const OrdersList: FC = () => {
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
@@ -24,13 +27,32 @@ const OrdersList: FC = () => {
|
|||||||
} = useOrderFilters()
|
} = useOrderFilters()
|
||||||
|
|
||||||
const { data: ordersData, isLoading } = useGetOrders(apiParams)
|
const { data: ordersData, isLoading } = useGetOrders(apiParams)
|
||||||
|
const { mutate: sendPleaseComeSms, isPending: isSendingSms } = useSendPleaseComeSms()
|
||||||
|
const [smsOrder, setSmsOrder] = useState<Order | null>(null)
|
||||||
|
|
||||||
const orders = ordersData?.data || []
|
const orders = ordersData?.data || []
|
||||||
const columns = getOrderTableColumns({ t })
|
const columns = getOrderTableColumns({
|
||||||
|
t,
|
||||||
|
onSendPleaseComeSms: (order) => setSmsOrder(order),
|
||||||
|
})
|
||||||
const filterFields = useOrderFiltersFields()
|
const filterFields = useOrderFiltersFields()
|
||||||
|
|
||||||
const totalPages = ordersData?.meta?.totalPages || 1
|
const totalPages = ordersData?.meta?.totalPages || 1
|
||||||
|
|
||||||
|
const handleConfirmSendSms = () => {
|
||||||
|
if (!smsOrder) return
|
||||||
|
|
||||||
|
sendPleaseComeSms(smsOrder.id, {
|
||||||
|
onSuccess: (response) => {
|
||||||
|
toast.success(response.message || 'پیامک در صف ارسال قرار گرفت')
|
||||||
|
setSmsOrder(null)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
@@ -64,6 +86,15 @@ const OrdersList: FC = () => {
|
|||||||
onPageChange: handlePageChange,
|
onPageChange: handlePageChange,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SendPleaseComeSmsModal
|
||||||
|
isOpen={!!smsOrder}
|
||||||
|
onClose={() => setSmsOrder(null)}
|
||||||
|
isLoading={isSendingSms}
|
||||||
|
customerName={smsOrder?.user?.firstName || ''}
|
||||||
|
restaurantName={smsOrder?.restaurant?.name || ''}
|
||||||
|
onConfirm={handleConfirmSendSms}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,144 +1,162 @@
|
|||||||
import Button from '@/components/Button'
|
import Button from "@/components/Button";
|
||||||
import { type FC, useState, useEffect } from 'react'
|
import { type FC, useState, useEffect } from "react";
|
||||||
|
|
||||||
interface OrderActionsProps {
|
interface OrderActionsProps {
|
||||||
onPrint: () => void
|
onPrint: () => void;
|
||||||
showSendToKitchenButton: boolean
|
showSendToKitchenButton: boolean;
|
||||||
showCancelButton: boolean
|
showCancelButton: boolean;
|
||||||
isPreparing: boolean
|
isPreparing: boolean;
|
||||||
isCourierDelivery: boolean
|
isCourierDelivery: boolean;
|
||||||
isDineInOrCarDelivery: boolean
|
isDineInOrCarDelivery: boolean;
|
||||||
isCustomerPickup: boolean
|
isCustomerPickup: boolean;
|
||||||
isCanceled: boolean
|
isCanceled: boolean;
|
||||||
canCompleteOrder: boolean
|
canCompleteOrder: boolean;
|
||||||
isChangingStatus: boolean
|
canSendPleaseComeSms: boolean;
|
||||||
onSendToKitchen: () => void
|
isChangingStatus: boolean;
|
||||||
onDeliverToCourier: () => void
|
isSendingPleaseComeSms?: boolean;
|
||||||
onDeliverToWaiter: () => void
|
onSendToKitchen: () => void;
|
||||||
onDeliverToReceptionist: () => void
|
onDeliverToCourier: () => void;
|
||||||
onCompleteOrder: () => void
|
onDeliverToWaiter: () => void;
|
||||||
onCancelOrder: () => void
|
onDeliverToReceptionist: () => void;
|
||||||
|
onCompleteOrder: () => void;
|
||||||
|
onCancelOrder: () => void;
|
||||||
|
onSendPleaseComeSms: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OrderActions: FC<OrderActionsProps> = ({
|
const OrderActions: FC<OrderActionsProps> = ({
|
||||||
onPrint,
|
onPrint,
|
||||||
showSendToKitchenButton,
|
showSendToKitchenButton,
|
||||||
showCancelButton,
|
showCancelButton,
|
||||||
isPreparing,
|
isPreparing,
|
||||||
isCourierDelivery,
|
isCourierDelivery,
|
||||||
isDineInOrCarDelivery,
|
isDineInOrCarDelivery,
|
||||||
isCustomerPickup,
|
isCustomerPickup,
|
||||||
isCanceled,
|
isCanceled,
|
||||||
canCompleteOrder,
|
canCompleteOrder,
|
||||||
isChangingStatus,
|
canSendPleaseComeSms,
|
||||||
onSendToKitchen,
|
isChangingStatus,
|
||||||
onDeliverToCourier,
|
isSendingPleaseComeSms,
|
||||||
onDeliverToWaiter,
|
onSendToKitchen,
|
||||||
onDeliverToReceptionist,
|
onDeliverToCourier,
|
||||||
onCompleteOrder,
|
onDeliverToWaiter,
|
||||||
onCancelOrder,
|
onDeliverToReceptionist,
|
||||||
|
onCompleteOrder,
|
||||||
|
onCancelOrder,
|
||||||
|
onSendPleaseComeSms,
|
||||||
}) => {
|
}) => {
|
||||||
const [loadingAction, setLoadingAction] = useState<string | null>(null)
|
const [loadingAction, setLoadingAction] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isChangingStatus) {
|
if (!isChangingStatus) {
|
||||||
setLoadingAction(null)
|
setLoadingAction(null);
|
||||||
}
|
|
||||||
}, [isChangingStatus])
|
|
||||||
|
|
||||||
const handleSendToKitchen = () => {
|
|
||||||
setLoadingAction('sendToKitchen')
|
|
||||||
onSendToKitchen()
|
|
||||||
}
|
}
|
||||||
|
}, [isChangingStatus]);
|
||||||
|
|
||||||
const handleDeliverToCourier = () => {
|
const handleSendToKitchen = () => {
|
||||||
setLoadingAction('deliverToCourier')
|
setLoadingAction("sendToKitchen");
|
||||||
onDeliverToCourier()
|
onSendToKitchen();
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleDeliverToWaiter = () => {
|
const handleDeliverToCourier = () => {
|
||||||
setLoadingAction('deliverToWaiter')
|
setLoadingAction("deliverToCourier");
|
||||||
onDeliverToWaiter()
|
onDeliverToCourier();
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleDeliverToReceptionist = () => {
|
const handleDeliverToWaiter = () => {
|
||||||
setLoadingAction('deliverToReceptionist')
|
setLoadingAction("deliverToWaiter");
|
||||||
onDeliverToReceptionist()
|
onDeliverToWaiter();
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleCancelOrder = () => {
|
const handleDeliverToReceptionist = () => {
|
||||||
setLoadingAction('cancelOrder')
|
setLoadingAction("deliverToReceptionist");
|
||||||
onCancelOrder()
|
onDeliverToReceptionist();
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleCompleteOrder = () => {
|
const handleCancelOrder = () => {
|
||||||
setLoadingAction('completeOrder')
|
setLoadingAction("cancelOrder");
|
||||||
onCompleteOrder()
|
onCancelOrder();
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
const handleCompleteOrder = () => {
|
||||||
<div className='mt-5 sm:mt-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-1 gap-3'>
|
setLoadingAction("completeOrder");
|
||||||
<Button
|
onCompleteOrder();
|
||||||
label='چاپ'
|
};
|
||||||
onClick={onPrint}
|
|
||||||
className='bg-slate-700 hover:bg-slate-800'
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!isCanceled && showSendToKitchenButton && (
|
return (
|
||||||
<Button
|
<div className="mt-5 sm:mt-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-1 gap-3">
|
||||||
label='ارسال به آشپزخانه'
|
<div className="flex gap-3">
|
||||||
onClick={handleSendToKitchen}
|
<Button
|
||||||
isloading={isChangingStatus && loadingAction === 'sendToKitchen'}
|
label="چاپ"
|
||||||
className='bg-orange-600 hover:bg-orange-700'
|
onClick={onPrint}
|
||||||
/>
|
className="border bg-white border-slate-700 text-slate-700 hover:bg-slate-100"
|
||||||
)}
|
/>
|
||||||
|
|
||||||
{!isCanceled && isPreparing && isCourierDelivery && (
|
{canSendPleaseComeSms && (
|
||||||
<Button
|
<Button
|
||||||
label='تحویل به پیک'
|
label="ارسال پیام"
|
||||||
onClick={handleDeliverToCourier}
|
onClick={onSendPleaseComeSms}
|
||||||
isloading={isChangingStatus && loadingAction === 'deliverToCourier'}
|
isloading={isSendingPleaseComeSms}
|
||||||
className='bg-blue-600 hover:bg-blue-700'
|
className="border bg-white border-teal-600 text-teal-600 hover:bg-teal-100"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{!isCanceled && showCancelButton && (
|
||||||
|
<Button
|
||||||
|
label="لغو سفارش"
|
||||||
|
onClick={handleCancelOrder}
|
||||||
|
isloading={isChangingStatus && loadingAction === "cancelOrder"}
|
||||||
|
className="bg-transparent border border-red-500 text-red-500 hover:bg-red-100 "
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{!isCanceled && isPreparing && isDineInOrCarDelivery && (
|
{!isCanceled && showSendToKitchenButton && (
|
||||||
<Button
|
<Button
|
||||||
label='تحویل به گارسون'
|
label="ارسال به آشپزخانه"
|
||||||
onClick={handleDeliverToWaiter}
|
onClick={handleSendToKitchen}
|
||||||
isloading={isChangingStatus && loadingAction === 'deliverToWaiter'}
|
isloading={isChangingStatus && loadingAction === "sendToKitchen"}
|
||||||
className='bg-purple-600 hover:bg-purple-700'
|
className="bg-orange-600 hover:bg-orange-700"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isCanceled && isPreparing && isCustomerPickup && (
|
{!isCanceled && isPreparing && isCourierDelivery && (
|
||||||
<Button
|
<Button
|
||||||
label='تحویل به رسپشنیست'
|
label="تحویل به پیک"
|
||||||
onClick={handleDeliverToReceptionist}
|
onClick={handleDeliverToCourier}
|
||||||
isloading={isChangingStatus && loadingAction === 'deliverToReceptionist'}
|
isloading={isChangingStatus && loadingAction === "deliverToCourier"}
|
||||||
className='bg-indigo-600 hover:bg-indigo-700'
|
className="bg-blue-600 hover:bg-blue-700"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isCanceled && canCompleteOrder && (
|
{!isCanceled && isPreparing && isDineInOrCarDelivery && (
|
||||||
<Button
|
<Button
|
||||||
label='اتمام سفارش'
|
label="تحویل به گارسون"
|
||||||
onClick={handleCompleteOrder}
|
onClick={handleDeliverToWaiter}
|
||||||
isloading={isChangingStatus && loadingAction === 'completeOrder'}
|
isloading={isChangingStatus && loadingAction === "deliverToWaiter"}
|
||||||
className='bg-green-600 hover:bg-green-700'
|
className="bg-purple-600 hover:bg-purple-700"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isCanceled && showCancelButton && (
|
{!isCanceled && isPreparing && isCustomerPickup && (
|
||||||
<Button
|
<Button
|
||||||
label='لغو سفارش'
|
label="تحویل به رسپشنیست"
|
||||||
onClick={handleCancelOrder}
|
onClick={handleDeliverToReceptionist}
|
||||||
isloading={isChangingStatus && loadingAction === 'cancelOrder'}
|
isloading={
|
||||||
className='bg-transparent border border-red-500 text-red-500 hover:bg-red-50 sm:col-span-2 lg:col-span-1'
|
isChangingStatus && loadingAction === "deliverToReceptionist"
|
||||||
/>
|
}
|
||||||
)}
|
className="bg-indigo-600 hover:bg-indigo-700"
|
||||||
</div>
|
/>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
|
|
||||||
export default OrderActions
|
{!isCanceled && canCompleteOrder && (
|
||||||
|
<Button
|
||||||
|
label="اتمام سفارش"
|
||||||
|
onClick={handleCompleteOrder}
|
||||||
|
isloading={isChangingStatus && loadingAction === "completeOrder"}
|
||||||
|
className="bg-green-600 hover:bg-green-700"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderActions;
|
||||||
|
|||||||
@@ -1,146 +1,189 @@
|
|||||||
import { Eye } from 'iconsax-react'
|
import { Eye, MessageText } from "iconsax-react";
|
||||||
import type { ColumnType } from '@/components/types/TableTypes'
|
import type { ColumnType } from "@/components/types/TableTypes";
|
||||||
import type { Order } from '../types/Types'
|
import type { Order } from "../types/Types";
|
||||||
import { formatPrice, formatOptionalDate } from '@/helpers/func'
|
import { formatPrice, formatOptionalDate } from "@/helpers/func";
|
||||||
import Status from '@/components/Status'
|
import Status from "@/components/Status";
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from "react-router-dom";
|
||||||
import { Pages } from '@/config/Pages'
|
import { Pages } from "@/config/Pages";
|
||||||
import { formatFaNumber } from '@/helpers/func'
|
import { formatFaNumber } from "@/helpers/func";
|
||||||
import type { TFunction } from 'i18next'
|
import type { TFunction } from "i18next";
|
||||||
|
|
||||||
interface GetOrderTableColumnsParams {
|
interface GetOrderTableColumnsParams {
|
||||||
onDelete?: (id: string) => void
|
onDelete?: (id: string) => void;
|
||||||
isDeleting?: boolean
|
isDeleting?: boolean;
|
||||||
t: TFunction
|
onSendPleaseComeSms?: (order: Order) => void;
|
||||||
|
t: TFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusVariant = (status: string): 'success' | 'error' | 'warning' | 'info' | 'pending' => {
|
const getStatusVariant = (
|
||||||
const variantMap: Record<string, 'success' | 'error' | 'warning' | 'info' | 'pending'> = {
|
status: string,
|
||||||
'new': 'pending',
|
): "success" | "error" | "warning" | "info" | "pending" => {
|
||||||
'preparing': 'warning',
|
const variantMap: Record<
|
||||||
'shipped': 'info',
|
string,
|
||||||
'completed': 'success',
|
"success" | "error" | "warning" | "info" | "pending"
|
||||||
'canceled': 'error',
|
> = {
|
||||||
}
|
new: "pending",
|
||||||
return variantMap[status] || 'pending'
|
preparing: "warning",
|
||||||
}
|
shipped: "info",
|
||||||
|
completed: "success",
|
||||||
|
canceled: "error",
|
||||||
|
};
|
||||||
|
return variantMap[status] || "pending";
|
||||||
|
};
|
||||||
|
|
||||||
const getShiftAdminName = (item: Order): string => {
|
const getShiftAdminName = (item: Order): string => {
|
||||||
const admin = item.cashShift?.admin
|
const admin = item.cashShift?.admin;
|
||||||
if (!admin) return '-'
|
if (!admin) return "-";
|
||||||
const name = [admin.firstName, admin.lastName].filter(Boolean).join(' ')
|
const name = [admin.firstName, admin.lastName].filter(Boolean).join(" ");
|
||||||
return name || admin.phone || '-'
|
return name || admin.phone || "-";
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getOrderTableColumns = ({ t }: GetOrderTableColumnsParams): ColumnType<Order>[] => {
|
export const getOrderTableColumns = ({
|
||||||
return [
|
t,
|
||||||
{
|
onSendPleaseComeSms,
|
||||||
key: 'orderNumber',
|
}: GetOrderTableColumnsParams): ColumnType<Order>[] => {
|
||||||
title: 'شماره',
|
return [
|
||||||
render: (item: Order) => {
|
{
|
||||||
return formatFaNumber(item.orderNumber)
|
key: "orderNumber",
|
||||||
}
|
title: "شماره",
|
||||||
},
|
render: (item: Order) => {
|
||||||
{
|
return formatFaNumber(item.orderNumber);
|
||||||
key: 'customerName',
|
},
|
||||||
title: 'نام مشتری',
|
},
|
||||||
render: (item: Order) => {
|
{
|
||||||
const name = [item.user?.firstName, item.user?.lastName].filter(Boolean).join(' ')
|
key: "customerName",
|
||||||
return name || '-'
|
title: "نام مشتری",
|
||||||
}
|
render: (item: Order) => {
|
||||||
},
|
const name = [item.user?.firstName, item.user?.lastName]
|
||||||
{
|
.filter(Boolean)
|
||||||
key: 'shiftAdmin',
|
.join(" ");
|
||||||
title: 'شیفت',
|
return name || "-";
|
||||||
render: (item: Order) => getShiftAdminName(item),
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'createdAt',
|
key: "shiftAdmin",
|
||||||
title: 'تاریخ',
|
title: "شیفت",
|
||||||
render: (item: Order) => {
|
render: (item: Order) => getShiftAdminName(item),
|
||||||
if (!item.createdAt) return '-'
|
},
|
||||||
return (
|
{
|
||||||
<div className="flex flex-col whitespace-nowrap">
|
key: "createdAt",
|
||||||
<span className="text-[11px] text-description">
|
title: "تاریخ",
|
||||||
{formatOptionalDate(item.createdAt, {
|
render: (item: Order) => {
|
||||||
year: 'numeric',
|
if (!item.createdAt) return "-";
|
||||||
month: '2-digit',
|
return (
|
||||||
day: '2-digit',
|
<div className="flex flex-col whitespace-nowrap">
|
||||||
})}
|
<span className="text-[11px] text-description">
|
||||||
</span>
|
{formatOptionalDate(item.createdAt, {
|
||||||
<span className="text-[15px] font-medium">
|
year: "numeric",
|
||||||
{formatOptionalDate(item.createdAt, {
|
month: "2-digit",
|
||||||
hour: '2-digit',
|
day: "2-digit",
|
||||||
minute: '2-digit',
|
})}
|
||||||
})}
|
</span>
|
||||||
</span>
|
<span className="text-[15px] font-medium">
|
||||||
</div>
|
{formatOptionalDate(item.createdAt, {
|
||||||
)
|
hour: "2-digit",
|
||||||
}
|
minute: "2-digit",
|
||||||
},
|
})}
|
||||||
{
|
</span>
|
||||||
key: 'methods',
|
</div>
|
||||||
title: 'تحویل / پرداخت',
|
);
|
||||||
render: (item: Order) => {
|
},
|
||||||
const rows = [
|
},
|
||||||
{ label: 'نوع تحویل', value: item.deliveryMethod?.description || '-' },
|
{
|
||||||
{ label: 'نوع پرداخت', value: item.paymentMethod?.description || '-' },
|
key: "methods",
|
||||||
]
|
title: "تحویل / پرداخت",
|
||||||
return (
|
render: (item: Order) => {
|
||||||
<div className="flex flex-col gap-1 text-[12px] whitespace-nowrap">
|
const rows = [
|
||||||
{rows.map((row) => (
|
{
|
||||||
<div key={row.label} className="flex items-center justify-between gap-3">
|
label: "نوع تحویل",
|
||||||
<span className="text-description">{row.label}</span>
|
value: item.deliveryMethod?.description || "-",
|
||||||
<span>{row.value}</span>
|
},
|
||||||
</div>
|
{
|
||||||
))}
|
label: "نوع پرداخت",
|
||||||
</div>
|
value: item.paymentMethod?.description || "-",
|
||||||
)
|
},
|
||||||
}
|
];
|
||||||
},
|
return (
|
||||||
{
|
<div className="flex flex-col gap-1 text-[12px] whitespace-nowrap">
|
||||||
key: 'amounts',
|
{rows.map((row) => (
|
||||||
title: 'مبالغ',
|
<div
|
||||||
render: (item: Order) => {
|
key={row.label}
|
||||||
const rows = [
|
className="flex items-center justify-between gap-3"
|
||||||
{ label: 'مبلغ کل', value: formatPrice(item.total) },
|
>
|
||||||
{ label: 'پرداخت شده', value: formatPrice(Number(item.paidAmount ?? 0)) },
|
<span className="text-description">{row.label}</span>
|
||||||
{ label: 'مانده', value: formatPrice(Number(item.balance ?? 0)) },
|
<span>{row.value}</span>
|
||||||
]
|
</div>
|
||||||
return (
|
))}
|
||||||
<div className="flex flex-col gap-1 text-[12px] whitespace-nowrap">
|
</div>
|
||||||
{rows.map((row) => (
|
);
|
||||||
<div key={row.label} className="flex items-center justify-between gap-3">
|
},
|
||||||
<span className="text-description">{row.label}</span>
|
},
|
||||||
<span>{row.value}</span>
|
{
|
||||||
</div>
|
key: "amounts",
|
||||||
))}
|
title: "مبالغ",
|
||||||
</div>
|
render: (item: Order) => {
|
||||||
)
|
const rows = [
|
||||||
}
|
{ label: "مبلغ کل", value: formatPrice(item.total) },
|
||||||
},
|
{
|
||||||
{
|
label: "پرداخت شده",
|
||||||
key: 'status',
|
value: formatPrice(Number(item.paidAmount ?? 0)),
|
||||||
title: 'وضعیت',
|
},
|
||||||
render: (item: Order) => {
|
{ label: "مانده", value: formatPrice(Number(item.balance ?? 0)) },
|
||||||
return (
|
];
|
||||||
<Status
|
return (
|
||||||
variant={getStatusVariant(item.status)}
|
<div className="flex flex-col gap-1 text-[12px] whitespace-nowrap">
|
||||||
label={t(`order_status.${item.status}`)}
|
{rows.map((row) => (
|
||||||
/>
|
<div
|
||||||
)
|
key={row.label}
|
||||||
}
|
className="flex items-center justify-between gap-3"
|
||||||
},
|
>
|
||||||
{
|
<span className="text-description">{row.label}</span>
|
||||||
key: 'details',
|
<span>{row.value}</span>
|
||||||
title: '',
|
</div>
|
||||||
render: (item: Order) => {
|
))}
|
||||||
return (
|
</div>
|
||||||
<Link to={Pages.orders.detail + item.id}>
|
);
|
||||||
<Eye size={20} color="#8C90A3" className="cursor-pointer" />
|
},
|
||||||
</Link>
|
},
|
||||||
)
|
{
|
||||||
}
|
key: "status",
|
||||||
},
|
title: "وضعیت",
|
||||||
]
|
render: (item: Order) => {
|
||||||
}
|
return (
|
||||||
|
<Status
|
||||||
|
variant={getStatusVariant(item.status)}
|
||||||
|
label={t(`order_status.${item.status}`)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "actions",
|
||||||
|
title: "",
|
||||||
|
render: (item: Order) => {
|
||||||
|
const canSendSms = Boolean(item.user?.phone);
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3 whitespace-nowrap">
|
||||||
|
{canSendSms && onSendPleaseComeSms && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSendPleaseComeSms(item)}
|
||||||
|
className="cursor-pointer text-[12px] text-teal-700 hover:text-teal-800 font-medium"
|
||||||
|
title="ارسال پیام به مشتری"
|
||||||
|
>
|
||||||
|
<MessageText
|
||||||
|
size={20}
|
||||||
|
color="#8C90A3"
|
||||||
|
className="cursor-pointer"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<Link to={Pages.orders.detail + item.id} title="جزئیات">
|
||||||
|
<Eye size={20} color="#8C90A3" className="cursor-pointer" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import DefaulModal from '@/components/DefaulModal'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
|
||||||
|
interface SendPleaseComeSmsModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
isLoading?: boolean
|
||||||
|
customerName: string
|
||||||
|
restaurantName: string
|
||||||
|
onConfirm: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SendPleaseComeSmsModal: FC<SendPleaseComeSmsModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
isLoading,
|
||||||
|
customerName,
|
||||||
|
restaurantName,
|
||||||
|
onConfirm,
|
||||||
|
}) => {
|
||||||
|
const previewMessage = `${customerName || '#NAME#'} عزیز لطفا به پیشخوان ${restaurantName || '#REST#'} مراجعه کنید.`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaulModal
|
||||||
|
open={isOpen}
|
||||||
|
close={onClose}
|
||||||
|
isHeader
|
||||||
|
title_header='ارسال پیام'
|
||||||
|
width={420}
|
||||||
|
>
|
||||||
|
<div className='mt-6'>
|
||||||
|
<p className='text-sm text-description text-center mb-4'>
|
||||||
|
متن پیامک:
|
||||||
|
</p>
|
||||||
|
<div className='rounded-xl border border-gray-200 bg-gray-50 px-4 py-5 text-center text-[15px] leading-8'>
|
||||||
|
{previewMessage}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-4 justify-center mt-10'>
|
||||||
|
<Button
|
||||||
|
label='ارسال'
|
||||||
|
onClick={onConfirm}
|
||||||
|
isloading={isLoading}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label='انصراف'
|
||||||
|
className='bg-transparent text-black border border-primary'
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaulModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SendPleaseComeSmsModal
|
||||||
@@ -155,3 +155,9 @@ export const useRemoveOrderItem = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useSendPleaseComeSms = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (orderId: string) => api.sendPleaseComeSms(orderId),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -191,3 +191,10 @@ export const getDailyOrderReport = async (
|
|||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sendPleaseComeSms = async (orderId: string): Promise<{ message: string }> => {
|
||||||
|
const { data } = await axios.post<{ message: string }>(
|
||||||
|
`/admin/orders/${orderId}/please-come`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Input from '@/components/Input'
|
|||||||
import Textarea from '@/components/Textarea'
|
import Textarea from '@/components/Textarea'
|
||||||
import UploadBox from '@/components/UploadBox'
|
import UploadBox from '@/components/UploadBox'
|
||||||
import DefaulModal from '@/components/DefaulModal'
|
import DefaulModal from '@/components/DefaulModal'
|
||||||
|
import SwitchComponent from '@/components/Switch'
|
||||||
import LocationPicker from './components/LocationPicker'
|
import LocationPicker from './components/LocationPicker'
|
||||||
import LocationInput from './components/LocationInput'
|
import LocationInput from './components/LocationInput'
|
||||||
import { TickCircle } from 'iconsax-react'
|
import { TickCircle } from 'iconsax-react'
|
||||||
@@ -27,6 +28,22 @@ const validationSchema = yup.object({
|
|||||||
.min(1, 'حداقل یک شماره تماس الزامی است')
|
.min(1, 'حداقل یک شماره تماس الزامی است')
|
||||||
.of(yup.string().required('شماره تماس الزامی است')),
|
.of(yup.string().required('شماره تماس الزامی است')),
|
||||||
instagram: yup.string().url('لینک اینستاگرام معتبر نیست').nullable(),
|
instagram: yup.string().url('لینک اینستاگرام معتبر نیست').nullable(),
|
||||||
|
domain: yup
|
||||||
|
.string()
|
||||||
|
.nullable()
|
||||||
|
.test('domain-or-url', 'آدرس وبسایت معتبر نیست', (value) => {
|
||||||
|
if (!value) return true
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (/^https?:\/\//i.test(trimmed)) {
|
||||||
|
try {
|
||||||
|
new URL(trimmed)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(\/.*)?$/.test(trimmed)
|
||||||
|
}),
|
||||||
address: yup.string().required('آدرس الزامی است'),
|
address: yup.string().required('آدرس الزامی است'),
|
||||||
latitude: yup.number().required('انتخاب موقعیت روی نقشه الزامی است'),
|
latitude: yup.number().required('انتخاب موقعیت روی نقشه الزامی است'),
|
||||||
longitude: yup.number().required('انتخاب موقعیت روی نقشه الزامی است'),
|
longitude: yup.number().required('انتخاب موقعیت روی نقشه الزامی است'),
|
||||||
@@ -51,6 +68,7 @@ const GeneralSettings: FC = () => {
|
|||||||
menuColor: '#000000',
|
menuColor: '#000000',
|
||||||
phones: [''],
|
phones: [''],
|
||||||
instagram: '',
|
instagram: '',
|
||||||
|
domain: '',
|
||||||
address: '',
|
address: '',
|
||||||
latitude: 0,
|
latitude: 0,
|
||||||
longitude: 0,
|
longitude: 0,
|
||||||
@@ -58,6 +76,7 @@ const GeneralSettings: FC = () => {
|
|||||||
logo: '',
|
logo: '',
|
||||||
establishedYear: null,
|
establishedYear: null,
|
||||||
serviceArea: undefined,
|
serviceArea: undefined,
|
||||||
|
enableAiChat: false,
|
||||||
},
|
},
|
||||||
validationSchema,
|
validationSchema,
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
@@ -110,6 +129,7 @@ const GeneralSettings: FC = () => {
|
|||||||
? [restaurantData.phone]
|
? [restaurantData.phone]
|
||||||
: [''],
|
: [''],
|
||||||
instagram: restaurantData.instagram || '',
|
instagram: restaurantData.instagram || '',
|
||||||
|
domain: restaurantData.domain || '',
|
||||||
address: restaurantData.address || '',
|
address: restaurantData.address || '',
|
||||||
latitude: restaurantData.latitude || 0,
|
latitude: restaurantData.latitude || 0,
|
||||||
longitude: restaurantData.longitude || 0,
|
longitude: restaurantData.longitude || 0,
|
||||||
@@ -117,6 +137,7 @@ const GeneralSettings: FC = () => {
|
|||||||
logo: restaurantData.logo || '',
|
logo: restaurantData.logo || '',
|
||||||
establishedYear: restaurantData.establishedYear ?? null,
|
establishedYear: restaurantData.establishedYear ?? null,
|
||||||
serviceArea: restaurantData.serviceArea || undefined,
|
serviceArea: restaurantData.serviceArea || undefined,
|
||||||
|
enableAiChat: restaurantData.enableAiChat ?? false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -203,6 +224,22 @@ const GeneralSettings: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-8'>
|
||||||
|
<Input
|
||||||
|
label='آدرس وبسایت (دامنه)'
|
||||||
|
name='domain'
|
||||||
|
value={formik.values.domain || ''}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
error_text={formik.touched.domain && formik.errors.domain ? formik.errors.domain : undefined}
|
||||||
|
placeholder='example.com یا https://example.com'
|
||||||
|
isNotRequired
|
||||||
|
/>
|
||||||
|
<p className='mt-2 text-xs text-description'>
|
||||||
|
در صورتی که دامنه خریداری کرده اید آدرس دامنه خریداری شده رو وارد کنید
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className='mt-8'>
|
||||||
<Textarea
|
<Textarea
|
||||||
label='آدرس'
|
label='آدرس'
|
||||||
@@ -243,7 +280,15 @@ const GeneralSettings: FC = () => {
|
|||||||
onPolygonSelect={(polygon) => formik.setFieldValue('serviceArea', polygon)}
|
onPolygonSelect={(polygon) => formik.setFieldValue('serviceArea', polygon)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* </div> */}
|
|
||||||
|
<div className='mt-8'>
|
||||||
|
<div className='text-sm mb-3'>چت هوش مصنوعی</div>
|
||||||
|
<SwitchComponent
|
||||||
|
active={formik.values.enableAiChat ?? false}
|
||||||
|
onChange={(value) => formik.setFieldValue('enableAiChat', value)}
|
||||||
|
label={formik.values.enableAiChat ? 'فعال' : 'غیرفعال'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mt-8 flex justify-end'>
|
<div className='mt-8 flex justify-end'>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import Tabs from "@/components/Tabs";
|
import Tabs from "@/components/Tabs";
|
||||||
import { Brush2, Money2, NotificationStatus, Setting4 } from "iconsax-react";
|
import { Brush2, Money2, NotificationStatus, ScanBarcode, Setting4 } from "iconsax-react";
|
||||||
import { useState, type FC } from "react";
|
import { useState, type FC } from "react";
|
||||||
import NotificationsList from "../notifications/List";
|
import NotificationsList from "../notifications/List";
|
||||||
import DesignSettings from "./components/DesignSettings";
|
import DesignSettings from "./components/DesignSettings";
|
||||||
|
import QrCodeSettings from "./components/QrCodeSettings";
|
||||||
import ScoreSetting from "./components/ScoreSetting";
|
import ScoreSetting from "./components/ScoreSetting";
|
||||||
import { SettingTabEnum } from "./enum/Enum";
|
import { SettingTabEnum } from "./enum/Enum";
|
||||||
import GeneralSettings from "./GeneralSettings";
|
import GeneralSettings from "./GeneralSettings";
|
||||||
@@ -37,6 +38,11 @@ const Setting: FC = () => {
|
|||||||
label: "تنظیمات طراحی",
|
label: "تنظیمات طراحی",
|
||||||
value: SettingTabEnum.DESIGN,
|
value: SettingTabEnum.DESIGN,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: <ScanBarcode color={activeTab === SettingTabEnum.QR_CODE ? "black" : "#8C90A3"} size={24} />,
|
||||||
|
label: "QR کد",
|
||||||
|
value: SettingTabEnum.QR_CODE,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
onChange={(value: string) => setActiveTab(value as SettingTabEnum)}
|
onChange={(value: string) => setActiveTab(value as SettingTabEnum)}
|
||||||
active={activeTab}
|
active={activeTab}
|
||||||
@@ -48,6 +54,7 @@ const Setting: FC = () => {
|
|||||||
{activeTab === SettingTabEnum.FINANCIAL && <ScoreSetting />}
|
{activeTab === SettingTabEnum.FINANCIAL && <ScoreSetting />}
|
||||||
{activeTab === SettingTabEnum.NOTIFICATION && <NotificationsList />}
|
{activeTab === SettingTabEnum.NOTIFICATION && <NotificationsList />}
|
||||||
{activeTab === SettingTabEnum.DESIGN && <DesignSettings />}
|
{activeTab === SettingTabEnum.DESIGN && <DesignSettings />}
|
||||||
|
{activeTab === SettingTabEnum.QR_CODE && <QrCodeSettings />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import Button from "@/components/Button";
|
||||||
|
import { MENU_SITE_ORIGIN } from "@/config/menuPreview";
|
||||||
|
import { DocumentDownload } from "iconsax-react";
|
||||||
|
import { QRCodeCanvas } from "qrcode.react";
|
||||||
|
import { useMemo, useRef, type FC } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { useGetRestaurant } from "../hooks/useSettingData";
|
||||||
|
|
||||||
|
const normalizeMenuUrl = (domain?: string | null, slug?: string | null): string => {
|
||||||
|
const trimmedDomain = domain?.trim();
|
||||||
|
|
||||||
|
if (trimmedDomain) {
|
||||||
|
if (/^https?:\/\//i.test(trimmedDomain)) {
|
||||||
|
return trimmedDomain.replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
return `https://${trimmedDomain.replace(/\/+$/, "")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slug) {
|
||||||
|
return `${MENU_SITE_ORIGIN}/${slug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const QrCodeSettings: FC = () => {
|
||||||
|
const { data: restaurant, isLoading } = useGetRestaurant();
|
||||||
|
const canvasWrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const menuUrl = useMemo(
|
||||||
|
() => normalizeMenuUrl(restaurant?.data?.domain, restaurant?.data?.slug),
|
||||||
|
[restaurant?.data?.domain, restaurant?.data?.slug],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
const canvas = canvasWrapperRef.current?.querySelector("canvas");
|
||||||
|
if (!canvas) {
|
||||||
|
toast.error("QR کد آماده نیست");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
const fileName = restaurant?.data?.slug || restaurant?.data?.name || "menu";
|
||||||
|
link.download = `qr-${fileName}.png`;
|
||||||
|
link.href = canvas.toDataURL("image/png");
|
||||||
|
link.click();
|
||||||
|
toast.success("QR کد با موفقیت دانلود شد");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-4xl p-8">
|
||||||
|
<div className="text-sm text-gray-500">در حال بارگذاری...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!menuUrl) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-4xl p-8">
|
||||||
|
<div className="text-lg font-light">QR کد منو</div>
|
||||||
|
<p className="mt-4 text-sm text-gray-500">
|
||||||
|
برای ساخت QR کد، ابتدا دامنه وبسایت را در تنظیمات عمومی وارد کنید یا از اسلاگ رستوران استفاده شود.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-4xl p-8">
|
||||||
|
<div className="text-lg font-light">QR کد منو</div>
|
||||||
|
<p className="mt-2 text-sm text-gray-500">
|
||||||
|
این QR کد به آدرس منوی دیجیتال رستوران شما لینک میشود. میتوانید آن را دانلود و چاپ کنید.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-col items-center gap-6">
|
||||||
|
<div
|
||||||
|
ref={canvasWrapperRef}
|
||||||
|
className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm"
|
||||||
|
>
|
||||||
|
<QRCodeCanvas
|
||||||
|
value={menuUrl}
|
||||||
|
size={256}
|
||||||
|
level="H"
|
||||||
|
includeMargin
|
||||||
|
bgColor="#FFFFFF"
|
||||||
|
fgColor="#000000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full max-w-md text-center">
|
||||||
|
<div className="text-xs text-gray-400 mb-1">آدرس منو</div>
|
||||||
|
<a
|
||||||
|
href={menuUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-sm text-primary break-all hover:underline"
|
||||||
|
dir="ltr"
|
||||||
|
>
|
||||||
|
{menuUrl}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="button" className="w-fit px-6" onClick={handleDownload}>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<DocumentDownload size={20} color="#fff" />
|
||||||
|
<span>دانلود QR کد</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QrCodeSettings;
|
||||||
@@ -5,4 +5,5 @@ export const enum SettingTabEnum {
|
|||||||
DELIVERY = "delivery",
|
DELIVERY = "delivery",
|
||||||
NOTIFICATION = "notification",
|
NOTIFICATION = "notification",
|
||||||
DESIGN = "design",
|
DESIGN = "design",
|
||||||
|
QR_CODE = "qr_code",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ export type UpdateRestaurantType = {
|
|||||||
longitude?: number;
|
longitude?: number;
|
||||||
description?: string;
|
description?: string;
|
||||||
establishedYear?: number | null;
|
establishedYear?: number | null;
|
||||||
|
domain?: string;
|
||||||
serviceArea?: GeoJSONPolygon;
|
serviceArea?: GeoJSONPolygon;
|
||||||
|
enableAiChat?: boolean;
|
||||||
score?: {
|
score?: {
|
||||||
purchaseAmount: string;
|
purchaseAmount: string;
|
||||||
purchaseScore: string;
|
purchaseScore: string;
|
||||||
@@ -50,6 +52,7 @@ export type Restaurant = {
|
|||||||
longitude: number | null;
|
longitude: number | null;
|
||||||
serviceArea: GeoJSONPolygon;
|
serviceArea: GeoJSONPolygon;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
enableAiChat: boolean;
|
||||||
establishedYear: number | null;
|
establishedYear: number | null;
|
||||||
phoneNumber: string | null;
|
phoneNumber: string | null;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import UpdateUserGroup from '@/pages/customers/groups/Update'
|
|||||||
import CampaignsList from '@/pages/customers/campaigns/List'
|
import CampaignsList from '@/pages/customers/campaigns/List'
|
||||||
import CreateCampaign from '@/pages/customers/campaigns/Create'
|
import CreateCampaign from '@/pages/customers/campaigns/Create'
|
||||||
import WalletTransactions from '@/pages/wallet/Transactions'
|
import WalletTransactions from '@/pages/wallet/Transactions'
|
||||||
|
import AiChatsList from '@/pages/aiChats/List'
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
|
|
||||||
const { hasSubMenu } = useSharedStore()
|
const { hasSubMenu } = useSharedStore()
|
||||||
@@ -137,6 +138,7 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Pages.statistics.list} element={<Statistics />} />
|
<Route path={Pages.statistics.list} element={<Statistics />} />
|
||||||
|
|
||||||
<Route path={Pages.wallet.transactions} element={<WalletTransactions />} />
|
<Route path={Pages.wallet.transactions} element={<WalletTransactions />} />
|
||||||
|
<Route path={Pages.aiChats.list} element={<AiChatsList />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+11
-1
@@ -1,6 +1,6 @@
|
|||||||
import { usePermissions } from '@/hooks/usePermissions'
|
import { usePermissions } from '@/hooks/usePermissions'
|
||||||
import { PermissionEnum } from '@/pages/roles/enum/Enum'
|
import { PermissionEnum } from '@/pages/roles/enum/Enum'
|
||||||
import { Calendar, Card, DocumentText, Element3, Gallery, Home2, Logout, NotificationBing, People, Security, SecurityUser, Setting2, SmsEdit, Star1, TicketDiscount, TruckFast } from 'iconsax-react'
|
import { Calendar, Card, DocumentText, Element3, Gallery, Home2, Logout, MessageQuestion, NotificationBing, People, Security, SecurityUser, Setting2, SmsEdit, Star1, TicketDiscount, TruckFast } from 'iconsax-react'
|
||||||
import { type FC, useEffect } from 'react'
|
import { type FC, useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Skeleton from 'react-loading-skeleton'
|
import Skeleton from 'react-loading-skeleton'
|
||||||
@@ -123,6 +123,16 @@ const SideBar: FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{checkPermission(PermissionEnum.MANAGE_FOODS) && (
|
||||||
|
<SideBarItem
|
||||||
|
icon={<MessageQuestion variant={isActive('ai-chats') ? 'Bold' : 'Outline'} color={isActive('ai-chats') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||||
|
title={t('sidebar.ai_chats')}
|
||||||
|
isActive={isActive('ai-chats')}
|
||||||
|
link={Pages.aiChats.list}
|
||||||
|
activeName='ai-chats'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{checkPermission(PermissionEnum.MANAGE_SCHEDULES) && (
|
{checkPermission(PermissionEnum.MANAGE_SCHEDULES) && (
|
||||||
<SideBarItem
|
<SideBarItem
|
||||||
icon={<Calendar variant={isActive('schedule') ? 'Bold' : 'Outline'} color={isActive('schedule') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
icon={<Calendar variant={isActive('schedule') ? 'Bold' : 'Outline'} color={isActive('schedule') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||||
|
|||||||
Reference in New Issue
Block a user