announcement

This commit is contained in:
hamid zarghami
2025-10-12 09:46:03 +03:30
parent c11c331fb9
commit e8c657f573
14 changed files with 538 additions and 24 deletions
+108
View File
@@ -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 (
<div className="flex justify-center items-center min-h-[400px]">
<PageLoading />
</div>
)
}
if (error) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<Error errorText="خطا در بارگذاری اطلاعیه‌ها" />
</div>
)
}
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 (
<div>
<div className='flex justify-end mt-5'>
<Button
label="ایجاد اطلاعیه جدید"
onClick={() => setIsModalOpen(true)}
className="w-auto"
/>
</div>
<div className='relative overflow-x-auto rounded-3xl mt-5 w-full'>
<table className='w-full text-sm'>
<thead className='thead'>
<tr>
<Td text={'عنوان'} />
<Td text={'متن اطلاعیه'} />
<Td text={'نوع'} />
<Td text={'تاریخ ایجاد'} />
<Td text={'وضعیت'} />
</tr>
</thead>
<tbody>
{notifications.length === 0 ? (
<tr className='tr'>
<td colSpan={5} className="text-center py-8 text-gray-500">
هیچ اطلاعیهای یافت نشد
</td>
</tr>
) : (
notifications.map((notification: Notification) => (
<AnnouncementTableRow
key={notification._id}
notification={notification}
/>
))
)}
</tbody>
</table>
</div>
<PaginationUi
pager={pager}
onPageChange={(page) => {
setCurrentPage(page)
}}
/>
<CreateAnnouncementModal
open={isModalOpen}
close={handleModalClose}
onSuccess={handleModalSuccess}
/>
</div>
)
}
export default Annoncement
+12 -2
View File
@@ -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;
@@ -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<Props> = ({ notification }) => {
const getTypeLabel = (type: string) => {
const typeMap: Record<string, string> = {
[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 (
<tr className='tr'>
<Td text="">
<div className="max-w-xs">
<div className="font-medium text-gray-900 truncate">
{notification.title}
</div>
</div>
</Td>
<Td text="">
<div className="max-w-sm">
<div className="flex items-start gap-2">
<MessageText size={16} color="#8C90A3" className="mt-0.5 flex-shrink-0" />
<span className="text-gray-600 text-sm leading-relaxed">
{truncateMessage(notification.message)}
</span>
</div>
</div>
</Td>
<Td text="">
<StatusWithText
variant={getTypeVariant(notification.type)}
text={getTypeLabel(notification.type)}
/>
</Td>
<Td text="">
<div className="flex items-center gap-2">
<Calendar size={16} color="#8C90A3" />
<span className="text-gray-600 text-sm">
{formatDate(notification.createdAt)}
</span>
</div>
</Td>
<Td text="">
<StatusWithText
variant={notification.read ? 'success' : 'warning'}
text={notification.read ? 'خوانده شده' : 'خوانده نشده'}
/>
</Td>
</tr>
)
}
export default AnnouncementTableRow
@@ -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<Props> = ({ open, close, onSuccess }) => {
const createMutation = useCreateAnnouncementSeller()
const createSpecificMutation = useCreateAnnouncementSpecificSeller()
const [selectedSellers, setSelectedSellers] = useState<string[]>([])
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<CreateAnnouncementSeller>({
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 (
<DefaulModal
open={open}
close={close}
isHeader={true}
title_header="ایجاد اطلاعیه جدید"
width={600}
>
<div className="space-y-6 w-[450px] mt-4">
<Input
label="عنوان اطلاعیه"
placeholder="عنوان اطلاعیه را وارد کنید"
{...formik.getFieldProps('title')}
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
/>
<Textarea
label="متن اطلاعیه"
placeholder="متن اطلاعیه را وارد کنید"
rows={4}
{...formik.getFieldProps('message')}
error_text={formik.touched.message && formik.errors.message ? formik.errors.message : ''}
/>
<Select
label="نوع اطلاعیه"
items={announcementTypes}
placeholder="نوع اطلاعیه را انتخاب کنید"
{...formik.getFieldProps('type')}
error_text={formik.touched.type && formik.errors.type ? formik.errors.type : ''}
/>
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">گیرندگان اطلاعیه</label>
<RadioGroup
items={[
{ label: 'ارسال به همه فروشندگان', value: true },
{ label: 'ارسال به فروشندگان خاص', value: false }
]}
selected={sendToAll}
onChange={(value) => {
setSendToAll(value as boolean)
setSelectedSellers([])
}}
/>
</div>
{!sendToAll && (
<MultiSelectSearchable
label="انتخاب فروشندگان"
placeholder={sellersLoading ? "در حال بارگذاری فروشندگان..." : sellersError ? "خطا در بارگذاری فروشندگان" : sellerOptions.length === 0 ? "فروشنده‌ای یافت نشد" : "فروشندگان مورد نظر را انتخاب کنید"}
options={sellerOptions.length > 0 ? sellerOptions : []}
value={selectedSellers}
onChange={setSelectedSellers}
error_text={sellersError ? 'خطا در بارگذاری فروشندگان' : selectedSellers.length === 0 && !sendToAll ? 'انتخاب حداقل یک فروشنده الزامی است' : ''}
loading={sellersLoading && sellerOptions.length === 0}
disabled={!!sellersError}
/>
)}
<div className="flex gap-3 pt-4">
<Button
label="لغو"
variant="outline"
onClick={close}
className="flex-1"
/>
<Button
label="ایجاد اطلاعیه"
onClick={() => formik.handleSubmit()}
isLoading={createMutation.isPending || createSpecificMutation.isPending}
disabled={!formik.isValid || createMutation.isPending || createSpecificMutation.isPending || (!sendToAll && selectedSellers.length === 0)}
className="flex-1"
/>
</div>
</div>
</DefaulModal>
)
}
export default CreateAnnouncementModal
@@ -1,21 +1,7 @@
import { type FC } from 'react'
import Td from '../../../components/Td'
import { Call, Card, User as UserIcon, Money, Eye } from 'iconsax-react'
interface WithdrawalRequestData {
wallet: {
_id: string;
seller: {
fullName: string;
email?: string;
phoneNumber: string;
};
balance: number;
};
withdrawals: unknown[];
lastPaidAmount: number | null;
lastPaidDate: string | null;
}
import type { WithdrawalRequestData } from '../WithdrawalRequest'
interface Props {
request: WithdrawalRequestData
+21 -1
View File
@@ -1,10 +1,11 @@
import * as api from "../service/SellerService";
import { useMutation, useQuery } from "@tanstack/react-query";
export const useGetSellers = (page: number = 1) => {
export const useGetSellers = (page: number = 1, enabled: boolean = true) => {
return useQuery({
queryKey: ["sellers", page],
queryFn: () => api.getSellers(page),
enabled,
});
};
@@ -39,3 +40,22 @@ export const useGetWithdrawalRequests = (page: number = 1) => {
queryFn: () => api.getWithdrawalRequests(page),
});
};
export const useGetAnnouncementSeller = (page: number = 1) => {
return useQuery({
queryKey: ["announcement-seller", page],
queryFn: () => api.getAnnouncementSeller(page),
});
};
export const useCreateAnnouncementSeller = () => {
return useMutation({
mutationFn: api.createAnnouncementSeller,
});
};
export const useCreateAnnouncementSpecificSeller = () => {
return useMutation({
mutationFn: api.createAnnouncementToSpecificSellers,
});
};
+29
View File
@@ -3,6 +3,9 @@ import type {
SellersResponse,
WholesaleRequestsResponse,
WithdrawalRequestsResponse,
CreateAnnouncementSeller,
NotificationsResponse,
CreateAnnouncementSpecificSeller,
} from "../types/Types";
export const getSellers = async (
@@ -48,3 +51,29 @@ export const getWithdrawalRequests = async (
);
return data;
};
export const getAnnouncementSeller = async (
page: number = 1
): Promise<NotificationsResponse> => {
const { data } = await axios.get(
`/admin/notification/allSeller?page=${page}`
);
return data;
};
export const createAnnouncementSeller = async (
params: CreateAnnouncementSeller
) => {
const { data } = await axios.post(`/admin/notification/allSeller`, params);
return data;
};
export const createAnnouncementToSpecificSellers = async (
params: CreateAnnouncementSpecificSeller
) => {
const { data } = await axios.post(
`/admin/notification/specific-sellers`,
params
);
return data;
};
+53
View File
@@ -159,3 +159,56 @@ export interface WithdrawalRequestsResponse {
walletData: WithdrawalRequest[];
};
}
export interface CreateAnnouncementSeller {
title: string;
message: string;
type: AnnouncementType;
}
export interface CreateAnnouncementSpecificSeller
extends CreateAnnouncementSeller {
sellerIds: string[];
}
export enum AnnouncementType {
Announcement = "announcement",
Order = "order",
Product = "product",
Shipment = "shipment",
Fine = "fine",
Wallet = "wallet",
Ticket = "ticket",
Chat = "chat",
Contract = "contract",
}
export interface NotificationPager {
page: number;
limit: number;
totalItems: number;
totalPages: number;
prevPage: boolean;
nextPage: boolean;
}
export interface Notification {
_id: string;
recipient: string;
type: string;
title: string;
message: string;
meta: unknown | null;
read: boolean;
createdAt: string;
updatedAt: string;
}
export interface NotificationsResponse {
status: number;
success: boolean;
results: {
pager: NotificationPager;
notifications: Notification[];
};
}