From e8c657f57344fdf6b63e533b400544ecf2953e27 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sun, 12 Oct 2025 09:46:03 +0330 Subject: [PATCH] announcement --- .env | 4 +- src/components/MultiSelectSearchable.tsx | 10 +- src/config/Pages.ts | 1 + src/index.css | 8 + src/pages/seller/Annoncement.tsx | 108 ++++++++++ src/pages/seller/WithdrawalRequest.tsx | 14 +- .../components/AnnouncementTableRow.tsx | 101 ++++++++++ .../components/CreateAnnouncementModal.tsx | 190 ++++++++++++++++++ .../components/WithdrawalRequestTableRow.tsx | 16 +- src/pages/seller/hooks/useSellerData.ts | 22 +- src/pages/seller/service/SellerService.ts | 29 +++ src/pages/seller/types/Types.ts | 53 +++++ src/router/Main.tsx | 2 + src/shared/SideBar.tsx | 4 +- 14 files changed, 538 insertions(+), 24 deletions(-) create mode 100644 src/pages/seller/Annoncement.tsx create mode 100644 src/pages/seller/components/AnnouncementTableRow.tsx create mode 100644 src/pages/seller/components/CreateAnnouncementModal.tsx diff --git a/.env b/.env index eaf93d1..bc7f37a 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -# VITE_BASE_URL = 'https://shop-api.dev.danakcorp.com' -VITE_BASE_URL = 'https://api.shinan.ir' +VITE_BASE_URL = 'https://shop-api.dev.danakcorp.com' +# VITE_BASE_URL = 'https://api.shinan.ir' VITE_TOKEN_NAME = 'sh_admin_token' VITE_REFRESH_TOKEN_NAME = 'sh_admin_refresh_token' \ No newline at end of file diff --git a/src/components/MultiSelectSearchable.tsx b/src/components/MultiSelectSearchable.tsx index 0ae4cf8..ca6bd1d 100644 --- a/src/components/MultiSelectSearchable.tsx +++ b/src/components/MultiSelectSearchable.tsx @@ -121,7 +121,7 @@ const MultiSelectSearchable: React.FC = ({ { // Prevent event bubbling that might cause navigation @@ -154,7 +154,13 @@ const MultiSelectSearchable: React.FC = ({ }} /> - + + {loading && options.length === 0 && ( +
+
+ در حال بارگذاری... +
+ )} موردی یافت نشد. {options.map((option) => { diff --git a/src/config/Pages.ts b/src/config/Pages.ts index 5c34bc4..42668a4 100644 --- a/src/config/Pages.ts +++ b/src/config/Pages.ts @@ -198,5 +198,6 @@ export const Pages = { list: "/sellers/list", wholesaleRequests: "/sellers/wholesale-requests", withdrawalRequests: "/sellers/withdrawal-requests", + announcement: "/sellers/announcement", }, }; diff --git a/src/index.css b/src/index.css index f57a549..c3b371f 100644 --- a/src/index.css +++ b/src/index.css @@ -196,3 +196,11 @@ textarea::placeholder { .rmdp-container { width: 100%; } + +* { + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* Internet Explorer 10+ */ +} +*::-webkit-scrollbar { + display: none; /* Safari and Chrome */ +} diff --git a/src/pages/seller/Annoncement.tsx b/src/pages/seller/Annoncement.tsx new file mode 100644 index 0000000..7bf527c --- /dev/null +++ b/src/pages/seller/Annoncement.tsx @@ -0,0 +1,108 @@ +import { type FC, useState } from 'react' +import { useGetAnnouncementSeller } from './hooks/useSellerData' +import { type Notification } from './types/Types' +import PageLoading from '../../components/PageLoading' +import Error from '../../components/Error' +import PaginationUi from '../../components/PaginationUi' +import Td from '../../components/Td' +import AnnouncementTableRow from './components/AnnouncementTableRow' +import Button from '@/components/Button' +import CreateAnnouncementModal from './components/CreateAnnouncementModal' + +const Annoncement: FC = () => { + const [currentPage, setCurrentPage] = useState(1) + const [isModalOpen, setIsModalOpen] = useState(false) + const { data: announcementsData, isLoading, error, refetch } = useGetAnnouncementSeller(currentPage) + + const handleModalClose = () => { + setIsModalOpen(false) + } + + const handleModalSuccess = () => { + refetch() + setCurrentPage(1) // Reset to first page after creating new announcement + } + + if (isLoading) { + return ( +
+ +
+ ) + } + + if (error) { + return ( +
+ +
+ ) + } + + const notifications = announcementsData?.results?.notifications || [] + const notificationPager = announcementsData?.results?.pager + + // Convert NotificationPager to PagerType expected by PaginationUi + const pager = notificationPager ? { + ...notificationPager, + nextPage: notificationPager.nextPage ? `${notificationPager.page + 1}` : null, + prevPage: notificationPager.prevPage + } : undefined + + return ( +
+
+
+ +
+ + + + + + + {notifications.length === 0 ? ( + + + + ) : ( + notifications.map((notification: Notification) => ( + + )) + )} + +
+ + + + +
+ هیچ اطلاعیه‌ای یافت نشد +
+
+ + { + setCurrentPage(page) + }} + /> + + +
+ ) +} + +export default Annoncement \ No newline at end of file diff --git a/src/pages/seller/WithdrawalRequest.tsx b/src/pages/seller/WithdrawalRequest.tsx index 69760ed..d99e925 100644 --- a/src/pages/seller/WithdrawalRequest.tsx +++ b/src/pages/seller/WithdrawalRequest.tsx @@ -7,15 +7,25 @@ import Td from '../../components/Td' import WithdrawalRequestTableRow from './components/WithdrawalRequestTableRow' import WithdrawalRequestModal from './components/WithdrawalRequestModal' -interface WithdrawalRequestData { +export interface WithdrawalRequestData { wallet: { _id: string; seller: { + _id: string; fullName: string; - email?: string; + dateOfBirth: string | null; phoneNumber: string; + accountStatus: "Pending" | "Approved"; + isRegisterCompleted: boolean; + businessType: string | null; + isWholesaler: boolean; + createdAt: string; + updatedAt: string; + email?: string; }; balance: number; + createdAt: string; + updatedAt: string; }; withdrawals: unknown[]; lastPaidAmount: number | null; diff --git a/src/pages/seller/components/AnnouncementTableRow.tsx b/src/pages/seller/components/AnnouncementTableRow.tsx new file mode 100644 index 0000000..31ecb30 --- /dev/null +++ b/src/pages/seller/components/AnnouncementTableRow.tsx @@ -0,0 +1,101 @@ +import { type FC } from 'react' +import Td from '../../../components/Td' +import { Calendar, MessageText } from 'iconsax-react' +import StatusWithText from '../../../components/StatusWithText' +import type { Notification } from '../types/Types' +import { AnnouncementType } from '../types/Types' + +interface Props { + notification: Notification +} + +const AnnouncementTableRow: FC = ({ notification }) => { + const getTypeLabel = (type: string) => { + const typeMap: Record = { + [AnnouncementType.Announcement]: 'اطلاعیه عمومی', + [AnnouncementType.Order]: 'سفارش', + [AnnouncementType.Product]: 'محصول', + [AnnouncementType.Shipment]: 'ارسال', + [AnnouncementType.Fine]: 'جریمه', + [AnnouncementType.Wallet]: 'کیف پول', + [AnnouncementType.Ticket]: 'تیکت', + [AnnouncementType.Chat]: 'چت', + [AnnouncementType.Contract]: 'قرارداد' + } + return typeMap[type] || type + } + + const getTypeVariant = (type: string) => { + switch (type) { + case AnnouncementType.Announcement: return 'warning' + case AnnouncementType.Order: return 'success' + case AnnouncementType.Product: return 'warning' + case AnnouncementType.Shipment: return 'success' + case AnnouncementType.Fine: return 'error' + case AnnouncementType.Wallet: return 'success' + case AnnouncementType.Ticket: return 'warning' + case AnnouncementType.Chat: return 'warning' + case AnnouncementType.Contract: return 'success' + default: return 'warning' + } + } + + const truncateMessage = (message: string, maxLength: number = 100) => { + if (message.length <= maxLength) return message + return message.substring(0, maxLength) + '...' + } + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('fa-IR', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + } + + return ( + + +
+
+ {notification.title} +
+
+ + +
+
+ + + {truncateMessage(notification.message)} + +
+
+ + + + + +
+ + + {formatDate(notification.createdAt)} + +
+ + + + + + ) +} + +export default AnnouncementTableRow diff --git a/src/pages/seller/components/CreateAnnouncementModal.tsx b/src/pages/seller/components/CreateAnnouncementModal.tsx new file mode 100644 index 0000000..4fe4186 --- /dev/null +++ b/src/pages/seller/components/CreateAnnouncementModal.tsx @@ -0,0 +1,190 @@ +import { type FC, useState, useMemo } from 'react' +import DefaulModal from '@/components/DefaulModal' +import Input from '@/components/Input' +import Textarea from '@/components/Textarea' +import Select, { type ItemsSelectType } from '@/components/Select' +import Button from '@/components/Button' +import MultiSelectSearchable, { type MultiSelectOption } from '@/components/MultiSelectSearchable' +import RadioGroup from '@/components/RadioGroup' +import { AnnouncementType, type CreateAnnouncementSeller, type CreateAnnouncementSpecificSeller } from '../types/Types' +import { useCreateAnnouncementSeller, useCreateAnnouncementSpecificSeller, useGetSellers } from '../hooks/useSellerData' +import { useFormik } from 'formik' +import * as Yup from 'yup' +import { toast } from 'react-toastify' +import { extractErrorMessage } from '@/helpers/utils' + +interface Props { + open: boolean + close: () => void + onSuccess?: () => void +} + +const CreateAnnouncementModal: FC = ({ open, close, onSuccess }) => { + const createMutation = useCreateAnnouncementSeller() + const createSpecificMutation = useCreateAnnouncementSpecificSeller() + const [selectedSellers, setSelectedSellers] = useState([]) + const [sendToAll, setSendToAll] = useState(true) + + // Get sellers for selection when modal is open + const { data: sellersData, isLoading: sellersLoading, error: sellersError } = useGetSellers(1) + + const announcementTypes: ItemsSelectType[] = [ + { value: AnnouncementType.Announcement, label: 'اطلاعیه عمومی' }, + { value: AnnouncementType.Order, label: 'سفارش' }, + { value: AnnouncementType.Product, label: 'محصول' }, + { value: AnnouncementType.Shipment, label: 'ارسال' }, + { value: AnnouncementType.Fine, label: 'جریمه' }, + { value: AnnouncementType.Wallet, label: 'کیف پول' }, + { value: AnnouncementType.Ticket, label: 'تیکت' }, + { value: AnnouncementType.Chat, label: 'چت' }, + { value: AnnouncementType.Contract, label: 'قرارداد' } + ] + + // Convert sellers to MultiSelectOption format + const sellerOptions: MultiSelectOption[] = useMemo(() => { + const sellers = sellersData?.results?.sellers || [] + return sellers + .filter(seller => seller.fullName && seller.phoneNumber) // Filter out sellers with missing data + .map(seller => ({ + value: seller._id, + label: `${seller.fullName} (${seller.phoneNumber})` + })) + }, [sellersData]) + + const formik = useFormik({ + initialValues: { + title: '', + message: '', + type: AnnouncementType.Announcement + }, + validationSchema: Yup.object({ + title: Yup.string().required('عنوان اطلاعیه الزامی است'), + message: Yup.string().required('متن اطلاعیه الزامی است'), + type: Yup.string().required('نوع اطلاعیه الزامی است') + }), + onSubmit: (values) => { + if (sendToAll) { + // Send to all sellers + createMutation.mutate(values, { + onSuccess: () => { + toast.success('اطلاعیه برای همه فروشندگان ارسال شد') + close() + onSuccess?.() + formik.resetForm() + setSelectedSellers([]) + setSendToAll(true) + }, + onError: (error) => { + toast.error(extractErrorMessage(error)) + } + }) + } else { + // Send to specific sellers + if (selectedSellers.length === 0) { + toast.error('لطفا حداقل یک فروشنده انتخاب کنید') + return + } + + const specificValues: CreateAnnouncementSpecificSeller = { + ...values, + sellerIds: selectedSellers + } + + createSpecificMutation.mutate(specificValues, { + onSuccess: () => { + toast.success(`اطلاعیه برای ${selectedSellers.length} فروشنده ارسال شد`) + close() + onSuccess?.() + formik.resetForm() + setSelectedSellers([]) + setSendToAll(true) + }, + onError: (error) => { + toast.error(extractErrorMessage(error)) + } + }) + } + } + }) + + return ( + +
+ + +