dkala ducatated from dmenu
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
VITE_TOKEN_NAME = 'admin_token'
|
VITE_TOKEN_NAME = 'admin_token'
|
||||||
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
|
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
|
||||||
VITE_BASE_URL = 'https://api.danakcorp.com'
|
VITE_BASE_URL = 'https://api.danakcorp.com'
|
||||||
# VITE_BASE_URL = 'http://192.168.1.100:4001'
|
# VITE_BASE_URL = 'http://10.86.60.88:3500'
|
||||||
@@ -136,6 +136,20 @@ export const Pages = {
|
|||||||
smsCountList: "/dmenu/sms-count/list",
|
smsCountList: "/dmenu/sms-count/list",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
dkala: {
|
||||||
|
icons: {
|
||||||
|
list: "/dkala/icons/list",
|
||||||
|
groupList: "/dkala/icons/group/list",
|
||||||
|
},
|
||||||
|
warnings: {
|
||||||
|
list: "/dkala/reports/list",
|
||||||
|
},
|
||||||
|
shops: {
|
||||||
|
list: "/dkala/shops/list",
|
||||||
|
update: "/dkala/shops/update/",
|
||||||
|
smsCountList: "/dkala/sms-count/list",
|
||||||
|
},
|
||||||
|
},
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
list: "/subscriptions/list",
|
list: "/subscriptions/list",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,4 +8,6 @@ export const SideBarItemHasSubMenu = [
|
|||||||
"blog",
|
"blog",
|
||||||
"support",
|
"support",
|
||||||
"accessLogs",
|
"accessLogs",
|
||||||
|
"dmenu",
|
||||||
|
"dkala",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -76,9 +76,11 @@
|
|||||||
"accessLogs": "لاگهای دسترسی",
|
"accessLogs": "لاگهای دسترسی",
|
||||||
"icons": "آیکون ها",
|
"icons": "آیکون ها",
|
||||||
"dmenu": "دی منو",
|
"dmenu": "دی منو",
|
||||||
|
"dkala": "دی کالا",
|
||||||
"groups_icon": "گروه های آیکون",
|
"groups_icon": "گروه های آیکون",
|
||||||
"reports": "گزارش ها",
|
"reports": "گزارش ها",
|
||||||
"restaurants": "رستوران ها",
|
"restaurants": "رستوران ها",
|
||||||
|
"shops": "فروشگاه ها",
|
||||||
"sms_count": "تعداد پیامک",
|
"sms_count": "تعداد پیامک",
|
||||||
"subscriptions": "اشتراک ها"
|
"subscriptions": "اشتراک ها"
|
||||||
},
|
},
|
||||||
@@ -895,6 +897,29 @@
|
|||||||
"subscription_end_date": "تاریخ پایان اشتراک",
|
"subscription_end_date": "تاریخ پایان اشتراک",
|
||||||
"days_remaining": "چند روز مانده"
|
"days_remaining": "چند روز مانده"
|
||||||
},
|
},
|
||||||
|
"shop": {
|
||||||
|
"list_shop": "لیست فروشگاهها",
|
||||||
|
"list_sms_count": "لیست تعداد پیامکهای فروشگاهها",
|
||||||
|
"name": "نام",
|
||||||
|
"phone": "شماره تماس",
|
||||||
|
"address": "آدرس",
|
||||||
|
"domain": "دامنه",
|
||||||
|
"status": "وضعیت",
|
||||||
|
"plan": "پلن",
|
||||||
|
"created_at": "تاریخ ایجاد",
|
||||||
|
"active": "فعال",
|
||||||
|
"inactive": "غیرفعال",
|
||||||
|
"admins": "مدیران",
|
||||||
|
"add_admin": "افزودن مدیر",
|
||||||
|
"sms_count": "تعداد پیامک",
|
||||||
|
"role": "نقش",
|
||||||
|
"slug": "اسلاگ",
|
||||||
|
"established_year": "سال تاسیس",
|
||||||
|
"subscription_id": "شناسه اشتراک",
|
||||||
|
"subscription_start_date": "تاریخ شروع اشتراک",
|
||||||
|
"subscription_end_date": "تاریخ پایان اشتراک",
|
||||||
|
"days_remaining": "چند روز مانده"
|
||||||
|
},
|
||||||
"subscription": {
|
"subscription": {
|
||||||
"list_subscriptions": "لیست اشتراکها",
|
"list_subscriptions": "لیست اشتراکها",
|
||||||
"service": "سرویس",
|
"service": "سرویس",
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import * as api from "../service/IconService";
|
||||||
|
import {
|
||||||
|
GroupIconsResponse,
|
||||||
|
ReportsResponse,
|
||||||
|
SystemRolesResponse,
|
||||||
|
ShopAdminsResponse,
|
||||||
|
UpdateShopType,
|
||||||
|
} from "../types/Types";
|
||||||
|
|
||||||
|
export const useGetIcons = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["dkala-icons"],
|
||||||
|
queryFn: api.getIcons,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateGroupIcon = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.createGroupIcon,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetGroupIcons = () => {
|
||||||
|
return useQuery<GroupIconsResponse>({
|
||||||
|
queryKey: ["dkala-group-icons"],
|
||||||
|
queryFn: api.getGroupIcons,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteGroupIcon = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => api.deleteGroupIcon(id),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateIcon = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.createIcon,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteIcon = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => api.deleteIcon(id),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetReports = () => {
|
||||||
|
return useQuery<ReportsResponse>({
|
||||||
|
queryKey: ["dkala-reports"],
|
||||||
|
queryFn: api.getReports,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetShops = (page: number = 1, limit: number = 10) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["shops", page, limit],
|
||||||
|
queryFn: () => api.getShops(page, limit),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetShopAdmins = (shopId: string) => {
|
||||||
|
return useQuery<ShopAdminsResponse>({
|
||||||
|
queryKey: ["shop-admins", shopId],
|
||||||
|
queryFn: () => api.getShopAdmins(shopId),
|
||||||
|
enabled: !!shopId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetShopSmsCount = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["shop-sms-count"],
|
||||||
|
queryFn: api.getShopSmsCount,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteShopAdmin = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
shopId,
|
||||||
|
adminId,
|
||||||
|
}: {
|
||||||
|
shopId: string;
|
||||||
|
adminId: string;
|
||||||
|
}) => api.deleteShopAdmin(shopId, adminId),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAddAdminShop = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.addAdminShop,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetSystemRoles = () => {
|
||||||
|
return useQuery<SystemRolesResponse>({
|
||||||
|
queryKey: ["dkala-system-roles"],
|
||||||
|
queryFn: api.getSystemRoles,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateShop = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
id,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
params: UpdateShopType;
|
||||||
|
}) => api.updateShop(id, params),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetShop = (id: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["shop", id],
|
||||||
|
queryFn: () => api.getShop(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useHardDeleteShop = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => api.hardDeleteShop(id),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { type FC, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import { Add } from 'iconsax-react'
|
||||||
|
import CreateGroupIcon from './components/CreateGroupIcon'
|
||||||
|
import { useGetGroupIcons, useDeleteGroupIcon } from '../hooks/useIconData'
|
||||||
|
import Td from '../../../components/Td'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import PageLoading from '../../../components/PageLoading'
|
||||||
|
import TrashWithConfrim from '../../../components/TrashWithConfrim'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
import { GroupIconType } from '../types/Types'
|
||||||
|
|
||||||
|
const GroupIconList: FC = () => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const [showModal, setShowModal] = useState<boolean>(false)
|
||||||
|
const [search, setSearch] = useState<string>('')
|
||||||
|
const { data: groupIcons, isLoading, refetch } = useGetGroupIcons()
|
||||||
|
const deleteGroupIcon = useDeleteGroupIcon()
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setShowModal(false)
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (id: string) => {
|
||||||
|
deleteGroupIcon.mutate(id, {
|
||||||
|
onSuccess: () => {
|
||||||
|
refetch()
|
||||||
|
toast.success(t('success'))
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error?.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = (groupIcons as unknown as { data?: GroupIconType[] })?.data || []
|
||||||
|
const filteredGroups = groups.filter((group: GroupIconType) =>
|
||||||
|
group.name.toLowerCase().includes(search.toLowerCase())
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4 min-h-[500px]'>
|
||||||
|
<div className='flex justify-between items-center'>
|
||||||
|
<div>
|
||||||
|
{t('icon.group_list')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className='w-[172px]'
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2 items-center'>
|
||||||
|
<Add size={20} color='white' />
|
||||||
|
<div>
|
||||||
|
{t('icon.add_new_group')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-4'>
|
||||||
|
<div className='flex justify-end items-center'>
|
||||||
|
<div className='xl:w-[300px] w-full'>
|
||||||
|
<Input
|
||||||
|
variant='search'
|
||||||
|
placeholder={t('ads.search')}
|
||||||
|
className='bg-white w-full'
|
||||||
|
onChangeSearchFinal={(value) => setSearch(value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
isLoading ?
|
||||||
|
<PageLoading />
|
||||||
|
:
|
||||||
|
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||||
|
<table className='w-full text-sm'>
|
||||||
|
<thead className='thead'>
|
||||||
|
<tr>
|
||||||
|
<Td text={t('icon.name')} />
|
||||||
|
<Td text={t('icon.icons_count')} />
|
||||||
|
<Td text={t('icon.created_at')} />
|
||||||
|
<Td text={t('')} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
filteredGroups.map((item: GroupIconType) => {
|
||||||
|
return (
|
||||||
|
<tr key={item.id} className='tr'>
|
||||||
|
<Td text={item.name} />
|
||||||
|
<Td text={item.icons?.length?.toString() || '0'} />
|
||||||
|
<Td text={new Date(item.createdAt).toLocaleDateString('fa-IR')} />
|
||||||
|
<Td text={t('')}>
|
||||||
|
<div className='flex gap-2 items-center'>
|
||||||
|
<TrashWithConfrim
|
||||||
|
onDelete={() => handleDelete(item.id)}
|
||||||
|
isLoading={deleteGroupIcon.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<CreateGroupIcon
|
||||||
|
open={showModal}
|
||||||
|
close={handleCloseModal}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GroupIconList
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { useState, type FC } from 'react'
|
||||||
|
import { useGetIcons, useDeleteIcon } from '../hooks/useIconData'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import { Add } from 'iconsax-react'
|
||||||
|
import CreateIcon from './components/CreateIcon'
|
||||||
|
import Td from '../../../components/Td'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import PageLoading from '../../../components/PageLoading'
|
||||||
|
import TrashWithConfrim from '../../../components/TrashWithConfrim'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
import { IconType } from '../types/Types'
|
||||||
|
import moment from 'moment-jalaali'
|
||||||
|
|
||||||
|
const IconsList: FC = () => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const [showModal, setShowModal] = useState<boolean>(false)
|
||||||
|
const [search, setSearch] = useState<string>('')
|
||||||
|
const { data: iconsData, isLoading, refetch } = useGetIcons()
|
||||||
|
const deleteIcon = useDeleteIcon()
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setShowModal(false)
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (id: string) => {
|
||||||
|
deleteIcon.mutate(id, {
|
||||||
|
onSuccess: () => {
|
||||||
|
refetch()
|
||||||
|
toast.success(t('success'))
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error?.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const icons = (iconsData as unknown as { data?: IconType[] })?.data || []
|
||||||
|
const filteredIcons = icons.filter((icon: IconType) =>
|
||||||
|
icon.url.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
icon.group?.name?.toLowerCase().includes(search.toLowerCase())
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4 min-h-[500px]'>
|
||||||
|
<div className='flex justify-between items-center'>
|
||||||
|
<div>
|
||||||
|
{t('icon.list_icon')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className='w-[172px]'
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2 items-center'>
|
||||||
|
<Add size={20} color='white' />
|
||||||
|
<div>
|
||||||
|
{t('icon.add_new_icon')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-4'>
|
||||||
|
<div className='flex justify-end items-center'>
|
||||||
|
<div className='xl:w-[300px] w-full'>
|
||||||
|
<Input
|
||||||
|
variant='search'
|
||||||
|
placeholder={t('ads.search')}
|
||||||
|
className='bg-white w-full'
|
||||||
|
onChangeSearchFinal={(value) => setSearch(value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
isLoading ?
|
||||||
|
<PageLoading />
|
||||||
|
:
|
||||||
|
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||||
|
<table className='w-full text-sm'>
|
||||||
|
<thead className='thead'>
|
||||||
|
<tr>
|
||||||
|
<Td text={t('icon.url')} />
|
||||||
|
<Td text={t('icon.group')} />
|
||||||
|
<Td text={t('icon.created_at')} />
|
||||||
|
<Td text={''} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
filteredIcons.map((item: IconType) => {
|
||||||
|
return (
|
||||||
|
<tr key={item.id} className='tr'>
|
||||||
|
<Td text={''}>
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<div className='size-10 rounded-lg overflow-hidden bg-gray-100 flex items-center justify-center'>
|
||||||
|
<img
|
||||||
|
src={item.url}
|
||||||
|
alt={item.url}
|
||||||
|
className='size-full object-contain'
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement
|
||||||
|
target.style.display = 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='truncate max-w-xs' title={item.url}>
|
||||||
|
{item.url}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text={item.group?.name || '-'} />
|
||||||
|
<Td text={moment(item.createdAt).format('jYYYY-jMM-jDD')} />
|
||||||
|
<Td text={''}>
|
||||||
|
<div className='flex gap-2 items-center'>
|
||||||
|
<TrashWithConfrim
|
||||||
|
onDelete={() => handleDelete(item.id)}
|
||||||
|
isLoading={deleteIcon.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<CreateIcon
|
||||||
|
open={showModal}
|
||||||
|
close={handleCloseModal}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IconsList
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Button from '../../../../components/Button'
|
||||||
|
import DefaulModal from '../../../../components/DefaulModal'
|
||||||
|
import Input from '../../../../components/Input'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { TickCircle } from 'iconsax-react'
|
||||||
|
import { useCreateGroupIcon } from '../../hooks/useIconData'
|
||||||
|
import { CreateGroupIconType } from '../../types/Types'
|
||||||
|
import { ErrorType } from '../../../../helpers/types'
|
||||||
|
|
||||||
|
interface CreateGroupIconProps {
|
||||||
|
open: boolean
|
||||||
|
close: () => void
|
||||||
|
onSuccess?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateGroupIcon: FC<CreateGroupIconProps> = ({ open, close, onSuccess }) => {
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const createGroupIcon = useCreateGroupIcon()
|
||||||
|
|
||||||
|
const formik = useFormik<CreateGroupIconType>({
|
||||||
|
initialValues: {
|
||||||
|
name: ''
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
name: Yup.string()
|
||||||
|
.required(t('errors.required'))
|
||||||
|
}),
|
||||||
|
onSubmit: (values) => {
|
||||||
|
createGroupIcon.mutate(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
formik.resetForm()
|
||||||
|
close()
|
||||||
|
toast.success(t('success'))
|
||||||
|
onSuccess?.()
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error?.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
formik.resetForm()
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaulModal
|
||||||
|
open={open}
|
||||||
|
close={handleClose}
|
||||||
|
title_header={t('icon.add_new_group')}
|
||||||
|
isHeader
|
||||||
|
>
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Input
|
||||||
|
label={t('title')}
|
||||||
|
className='bg-white bg-opacity-30'
|
||||||
|
name='name'
|
||||||
|
value={formik.values.name}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.name && formik.errors.name ? formik.errors.name : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='mt-12 border-t border-border pt-5 flex justify-end'>
|
||||||
|
<div className='flex gap-5 items-center'>
|
||||||
|
<Button
|
||||||
|
className='w-[150px] bg-white bg-opacity-15 text-description'
|
||||||
|
label={t('cancel')}
|
||||||
|
onClick={handleClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={createGroupIcon.isPending}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<TickCircle
|
||||||
|
size={20}
|
||||||
|
color='white'
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{t('submit')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaulModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateGroupIcon
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Button from '../../../../components/Button'
|
||||||
|
import DefaulModal from '../../../../components/DefaulModal'
|
||||||
|
import UploadBox from '../../../../components/UploadBox'
|
||||||
|
import Select from '../../../../components/Select'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { TickCircle } from 'iconsax-react'
|
||||||
|
import { useCreateIcon, useGetGroupIcons } from '../../hooks/useIconData'
|
||||||
|
import { useSingleUpload } from '../../../service/hooks/useServiceData'
|
||||||
|
import { CreateIconType, GroupIconType } from '../../types/Types'
|
||||||
|
import { ErrorType } from '../../../../helpers/types'
|
||||||
|
|
||||||
|
interface CreateIconProps {
|
||||||
|
open: boolean
|
||||||
|
close: () => void
|
||||||
|
onSuccess?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const [file, setFile] = useState<File>()
|
||||||
|
const createIcon = useCreateIcon()
|
||||||
|
const singleUpload = useSingleUpload()
|
||||||
|
const { data: groupIcons } = useGetGroupIcons()
|
||||||
|
|
||||||
|
const groups = (groupIcons as unknown as { data?: GroupIconType[] })?.data || []
|
||||||
|
const groupOptions = groups.map((group: GroupIconType) => ({
|
||||||
|
value: group.id,
|
||||||
|
label: group.name
|
||||||
|
}))
|
||||||
|
|
||||||
|
const handleCreateIcon = (values: CreateIconType) => {
|
||||||
|
createIcon.mutate(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
formik.resetForm()
|
||||||
|
setFile(undefined)
|
||||||
|
close()
|
||||||
|
toast.success(t('success'))
|
||||||
|
onSuccess?.()
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error?.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const formik = useFormik<CreateIconType>({
|
||||||
|
initialValues: {
|
||||||
|
url: '',
|
||||||
|
groupId: ''
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
groupId: Yup.string()
|
||||||
|
.required(t('errors.required'))
|
||||||
|
}),
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
if (!file) {
|
||||||
|
toast.error(t('errors.required'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
await singleUpload.mutateAsync(formData, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
values.url = data?.data?.url
|
||||||
|
handleCreateIcon(values)
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error?.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
formik.resetForm()
|
||||||
|
setFile(undefined)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaulModal
|
||||||
|
open={open}
|
||||||
|
close={handleClose}
|
||||||
|
title_header={t('icon.add_new_icon')}
|
||||||
|
isHeader
|
||||||
|
>
|
||||||
|
<div className='mt-6'>
|
||||||
|
<UploadBox
|
||||||
|
label={t('icon.url')}
|
||||||
|
onChange={(files) => setFile(files[0])}
|
||||||
|
isReset={!open}
|
||||||
|
accept="image/svg+xml"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='mt-4'>
|
||||||
|
<Select
|
||||||
|
label={t('icon.group')}
|
||||||
|
className='bg-white bg-opacity-30'
|
||||||
|
items={groupOptions}
|
||||||
|
placeholder={t('icon.select_group')}
|
||||||
|
{...formik.getFieldProps('groupId')}
|
||||||
|
error_text={formik.touched.groupId && formik.errors.groupId ? formik.errors.groupId : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-12 border-t border-border pt-5 flex justify-end'>
|
||||||
|
<div className='flex gap-5 items-center'>
|
||||||
|
<Button
|
||||||
|
className='w-[150px] bg-white bg-opacity-15 text-description'
|
||||||
|
label={t('cancel')}
|
||||||
|
onClick={handleClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={createIcon.isPending || singleUpload.isPending}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<TickCircle
|
||||||
|
size={20}
|
||||||
|
color='white'
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{t('submit')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaulModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateIcon
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useGetReports } from '../hooks/useIconData'
|
||||||
|
import PageLoading from '../../../components/PageLoading'
|
||||||
|
import Td from '../../../components/Td'
|
||||||
|
import { ReportItem } from '../types/Types'
|
||||||
|
import moment from 'moment-jalaali'
|
||||||
|
import { Eye } from 'iconsax-react'
|
||||||
|
import DefaulModal from '../../../components/DefaulModal'
|
||||||
|
|
||||||
|
const WarningsList: FC = () => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const { data: reports, isLoading } = useGetReports()
|
||||||
|
const [selectedReport, setSelectedReport] = useState<ReportItem | null>(null)
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const reportsList = (reports as { data?: ReportItem[] })?.data || []
|
||||||
|
|
||||||
|
const handleViewReport = (report: ReportItem) => {
|
||||||
|
setSelectedReport(report)
|
||||||
|
setIsModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsModalOpen(false)
|
||||||
|
setSelectedReport(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4'>
|
||||||
|
<div className='flex w-full justify-between items-center'>
|
||||||
|
<div>
|
||||||
|
{t('report.list_report')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
isLoading ?
|
||||||
|
<PageLoading />
|
||||||
|
:
|
||||||
|
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||||
|
<table className='w-full text-sm '>
|
||||||
|
<thead className='thead'>
|
||||||
|
<tr>
|
||||||
|
<Td text={t('report.subject')} />
|
||||||
|
<Td text={t('report.content')} />
|
||||||
|
<Td text={t('report.phone')} />
|
||||||
|
<Td text={t('report.scope')} />
|
||||||
|
<Td text={t('report.status')} />
|
||||||
|
<Td text={t('report.date')} />
|
||||||
|
<Td text={t('report.detail')} />
|
||||||
|
<Td text={''} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
reportsList.map((item: ReportItem) => {
|
||||||
|
return (
|
||||||
|
<tr key={item.id} className='tr'>
|
||||||
|
<Td text={item.subject} />
|
||||||
|
<Td text={item.content?.substring(0, 50) + (item.content?.length > 50 ? '...' : '')} />
|
||||||
|
<Td text={item.phone || '-'} />
|
||||||
|
<Td text={item.scope} />
|
||||||
|
<Td text={item.status} />
|
||||||
|
<Td text={''}>
|
||||||
|
<div className='dltr text-right'>
|
||||||
|
{moment(item.createdAt).format('jYYYY-jMM-jDD HH:mm')}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text={''}>
|
||||||
|
<div className='flex gap-2 items-center'>
|
||||||
|
<Eye
|
||||||
|
size={20}
|
||||||
|
color='#8C90A3'
|
||||||
|
className='cursor-pointer'
|
||||||
|
onClick={() => handleViewReport(item)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text={''} />
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{reportsList.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="text-gray-500 text-lg">هیچ گزارشی یافت نشد</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{/* مودال نمایش متن کامل گزارش */}
|
||||||
|
<DefaulModal
|
||||||
|
open={isModalOpen}
|
||||||
|
close={closeModal}
|
||||||
|
isHeader={true}
|
||||||
|
title_header={t('report.detail')}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
{selectedReport && (
|
||||||
|
<div className="space-y-6 mt-6">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-600 mb-2">{t('report.subject')}</div>
|
||||||
|
<div className="text-base text-gray-900">{selectedReport.subject}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-600 mb-2">{t('report.content')}</div>
|
||||||
|
<div className="text-base text-gray-900 whitespace-pre-wrap">{selectedReport.content}</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{selectedReport.phone && (
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-600 mb-2">{t('report.phone')}</div>
|
||||||
|
<div className="text-base text-gray-900">{selectedReport.phone}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-600 mb-2">{t('report.scope')}</div>
|
||||||
|
<div className="text-base text-gray-900">{selectedReport.scope}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-600 mb-2">{t('report.status')}</div>
|
||||||
|
<div className="text-base text-gray-900">{selectedReport.status}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-600 mb-2">{t('report.date')}</div>
|
||||||
|
<div className="text-base text-gray-900 dltr text-right">
|
||||||
|
{moment(selectedReport.createdAt).format('jYYYY-jMM-jDD HH:mm')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DefaulModal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WarningsList
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import axios from "../../../config/axios";
|
||||||
|
import {
|
||||||
|
CreateGroupIconType,
|
||||||
|
CreateIconType,
|
||||||
|
GroupIconsResponse,
|
||||||
|
IconsResponse,
|
||||||
|
ReportsResponse,
|
||||||
|
ShopsResponse,
|
||||||
|
ShopResponse,
|
||||||
|
ShopSmsCountResponse,
|
||||||
|
AddAdminShopType,
|
||||||
|
SystemRolesResponse,
|
||||||
|
ShopAdminsResponse,
|
||||||
|
UpdateShopType,
|
||||||
|
} from "../types/Types";
|
||||||
|
|
||||||
|
export const getIcons = async (): Promise<IconsResponse> => {
|
||||||
|
const { data } = await axios.get("/admin/dkala/icons");
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createGroupIcon = async (params: CreateGroupIconType) => {
|
||||||
|
const { data } = await axios.post("/admin/dkala/icons/groups", params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGroupIcons = async (): Promise<GroupIconsResponse> => {
|
||||||
|
const { data } = await axios.get("/admin/dkala/icons/groups");
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteGroupIcon = async (id: string) => {
|
||||||
|
const { data } = await axios.delete(`/admin/dkala/icons/groups/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createIcon = async (params: CreateIconType) => {
|
||||||
|
const { data } = await axios.post("/admin/dkala/icons", params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteIcon = async (id: string) => {
|
||||||
|
const { data } = await axios.delete(`/admin/dkala/icons/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getReports = async (): Promise<ReportsResponse> => {
|
||||||
|
const { data } = await axios.get("/admin/dkala/contacts");
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getShops = async (
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 10
|
||||||
|
): Promise<ShopsResponse> => {
|
||||||
|
const { data } = await axios.get(
|
||||||
|
`/admin/dkala/shops?page=${page}&limit=${limit}`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getShopAdmins = async (
|
||||||
|
shopId: string
|
||||||
|
): Promise<ShopAdminsResponse> => {
|
||||||
|
const { data } = await axios.get(
|
||||||
|
`/admin/dkala/shops/${shopId}/admins`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getShopSmsCount = async (): Promise<
|
||||||
|
ShopSmsCountResponse
|
||||||
|
> => {
|
||||||
|
const { data } = await axios.get(`/admin/dkala/shops/sms-count`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteShopAdmin = async (
|
||||||
|
shopId: string,
|
||||||
|
adminId: string
|
||||||
|
) => {
|
||||||
|
const { data } = await axios.delete(
|
||||||
|
`/admin/dkala/shops/${shopId}/admins/${adminId}`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addAdminShop = async (params: AddAdminShopType) => {
|
||||||
|
const { data } = await axios.post(
|
||||||
|
`/admin/dkala/shops/${params.shopId}/admins`,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSystemRoles = async (): Promise<SystemRolesResponse> => {
|
||||||
|
const { data } = await axios.get(`/admin/dkala/system-roles`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateShop = async (
|
||||||
|
id: string,
|
||||||
|
params: UpdateShopType
|
||||||
|
) => {
|
||||||
|
const { data } = await axios.patch(`/admin/dkala/shops/${id}`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getShop = async (
|
||||||
|
id: string
|
||||||
|
): Promise<ShopResponse> => {
|
||||||
|
const { data } = await axios.get(`/admin/dkala/shops/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hardDeleteShop = async (id: string) => {
|
||||||
|
const { data } = await axios.delete(
|
||||||
|
`/admin/dkala/shops/${id}/hard-delete`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useGetShops, useHardDeleteShop } from '../hooks/useIconData'
|
||||||
|
import PageLoading from '../../../components/PageLoading'
|
||||||
|
import Td from '../../../components/Td'
|
||||||
|
import { ShopType, ShopsResponse } from '../types/Types'
|
||||||
|
import moment from 'moment-jalaali'
|
||||||
|
import { Edit } from 'iconsax-react'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import ShopAdminsModal from './components/ShopAdminsModal'
|
||||||
|
import Pagination from '../../../components/Pagination'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { Pages } from '../../../config/Pages'
|
||||||
|
import TrashWithConfrim from '../../../components/TrashWithConfrim'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
const ShopsList: FC = () => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [page, setPage] = useState<number>(1)
|
||||||
|
const limit = 10
|
||||||
|
const { data: shops, isLoading } = useGetShops(page, limit)
|
||||||
|
const [selectedShopId, setSelectedShopId] = useState<string>('')
|
||||||
|
const [isAdminsModalOpen, setIsAdminsModalOpen] = useState<boolean>(false)
|
||||||
|
const deleteShop = useHardDeleteShop()
|
||||||
|
|
||||||
|
const shopsList = (shops as ShopsResponse | undefined)?.data?.shops || []
|
||||||
|
const totalPages = (shops as ShopsResponse | undefined)?.data?.pager?.totalPages || 1
|
||||||
|
|
||||||
|
const handleOpenAdminsModal = (shopId: string) => {
|
||||||
|
setSelectedShopId(shopId)
|
||||||
|
setIsAdminsModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseAdminsModal = () => {
|
||||||
|
setIsAdminsModalOpen(false)
|
||||||
|
setSelectedShopId('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateDaysRemaining = (endDate: string): string => {
|
||||||
|
const end = moment(endDate)
|
||||||
|
const now = moment()
|
||||||
|
const diff = end.diff(now, 'days')
|
||||||
|
if (diff < 0) {
|
||||||
|
return 'منقضی شده'
|
||||||
|
}
|
||||||
|
return `${diff} روز`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatJalaliDate = (date: string): string => {
|
||||||
|
return moment(date).format('jYYYY-jMM-jDD')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteShop = (shopId: string) => {
|
||||||
|
deleteShop.mutate(shopId, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('فروشگاه با موفقیت حذف شد')
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['shops'] })
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error('خطا در حذف فروشگاه')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4'>
|
||||||
|
<div className='flex w-full justify-between items-center'>
|
||||||
|
<div>
|
||||||
|
{t('shop.list_shop')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
isLoading ?
|
||||||
|
<PageLoading />
|
||||||
|
:
|
||||||
|
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||||
|
<table className='w-full text-sm '>
|
||||||
|
<thead className='thead'>
|
||||||
|
<tr>
|
||||||
|
<Td text={t('shop.name')} />
|
||||||
|
<Td text={t('shop.phone')} />
|
||||||
|
<Td text={t('shop.slug')} />
|
||||||
|
<Td text={t('shop.plan')} />
|
||||||
|
<Td text={t('shop.status')} />
|
||||||
|
<Td text={t('shop.subscription_start_date')} />
|
||||||
|
<Td text={t('shop.subscription_end_date')} />
|
||||||
|
<Td text={t('shop.days_remaining')} />
|
||||||
|
<Td text={''} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
shopsList.map((item: ShopType) => {
|
||||||
|
return (
|
||||||
|
<tr key={item.id} className='tr'>
|
||||||
|
<Td text={item.name} />
|
||||||
|
<Td text={item.phone || '-'} />
|
||||||
|
<Td text={item?.slug || '-'} />
|
||||||
|
<Td text={item.plan || '-'} />
|
||||||
|
<Td text={''}>
|
||||||
|
<div className={`w-fit px-3 py-1 rounded-2xl text-xs ${item.isActive
|
||||||
|
? 'bg-green-100 text-green-700'
|
||||||
|
: 'bg-red-100 text-red-700'
|
||||||
|
}`}>
|
||||||
|
{item.isActive ? t('shop.active') : t('shop.inactive')}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text={''}>
|
||||||
|
<div className='dltr text-right'>
|
||||||
|
{formatJalaliDate(item.subscriptionStartDate)}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text={''}>
|
||||||
|
<div className='dltr text-right'>
|
||||||
|
{formatJalaliDate(item.subscriptionEndDate)}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text={''}>
|
||||||
|
<div className='dltr text-right'>
|
||||||
|
{calculateDaysRemaining(item.subscriptionEndDate)}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text={''}>
|
||||||
|
<div className='flex items-center gap-4'>
|
||||||
|
<TrashWithConfrim
|
||||||
|
onDelete={() => handleDeleteShop(item.id)}
|
||||||
|
isLoading={deleteShop.isPending}
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
to={Pages.dkala.shops.update + item.id}
|
||||||
|
>
|
||||||
|
<Edit
|
||||||
|
size={20}
|
||||||
|
color='#8C90A3'
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
<Button
|
||||||
|
className='w-fit px-4'
|
||||||
|
onClick={() => handleOpenAdminsModal(item.id)}
|
||||||
|
>
|
||||||
|
{t('shop.admins')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Pagination
|
||||||
|
currentPage={page}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<ShopAdminsModal
|
||||||
|
open={isAdminsModalOpen}
|
||||||
|
close={handleCloseAdminsModal}
|
||||||
|
shopId={selectedShopId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShopsList
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useGetShopSmsCount } from '../hooks/useIconData'
|
||||||
|
import PageLoading from '../../../components/PageLoading'
|
||||||
|
import Td from '../../../components/Td'
|
||||||
|
import { ShopSmsCountItem } from '../types/Types'
|
||||||
|
|
||||||
|
const SmsCountList: FC = () => {
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const { data: smsCount, isLoading } = useGetShopSmsCount()
|
||||||
|
|
||||||
|
const smsCountList = (smsCount as { data?: ShopSmsCountItem[] })?.data || []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4'>
|
||||||
|
<div className='flex w-full justify-between items-center'>
|
||||||
|
<div>
|
||||||
|
{t('shop.list_sms_count')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
isLoading ?
|
||||||
|
<PageLoading />
|
||||||
|
:
|
||||||
|
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||||
|
<table className='w-full text-sm '>
|
||||||
|
<thead className='thead'>
|
||||||
|
<tr>
|
||||||
|
<Td text={t('shop.name')} />
|
||||||
|
<Td text={t('shop.sms_count')} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
smsCountList.map((item: ShopSmsCountItem) => {
|
||||||
|
return (
|
||||||
|
<tr key={item.shopId} className='tr'>
|
||||||
|
<Td text={item.shopName} />
|
||||||
|
<Td text={item.smsCount.toString()} />
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SmsCountList
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import { useFormik } from 'formik'
|
||||||
|
import { FC, useEffect } from 'react'
|
||||||
|
import { UpdateShopType, ShopResponse } from '../types/Types'
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
|
import { useGetShop, useUpdateShop } from '../hooks/useIconData'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import Select from '../../../components/Select'
|
||||||
|
import SwitchComponent from '../../../components/Switch'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import DatePickerComponent from '../../../components/DatePicker'
|
||||||
|
import PageLoading from '../../../components/PageLoading'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
import { TickCircle } from 'iconsax-react'
|
||||||
|
import { Pages } from '../../../config/Pages'
|
||||||
|
import moment from 'moment-jalaali'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
const UpdateShop: FC = () => {
|
||||||
|
|
||||||
|
const { id } = useParams()
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const { data: shopData, isLoading } = useGetShop(id || '')
|
||||||
|
const updateShop = useUpdateShop()
|
||||||
|
|
||||||
|
const shop = (shopData as ShopResponse | undefined)?.data
|
||||||
|
|
||||||
|
const formik = useFormik<UpdateShopType>({
|
||||||
|
initialValues: {
|
||||||
|
name: undefined,
|
||||||
|
slug: undefined,
|
||||||
|
establishedYear: undefined,
|
||||||
|
phone: undefined,
|
||||||
|
plan: undefined,
|
||||||
|
subscriptionId: undefined,
|
||||||
|
subscriptionEndDate: undefined,
|
||||||
|
subscriptionStartDate: undefined,
|
||||||
|
isActive: undefined,
|
||||||
|
},
|
||||||
|
onSubmit: (values) => {
|
||||||
|
if (!id) return
|
||||||
|
|
||||||
|
const params: UpdateShopType = {
|
||||||
|
...values,
|
||||||
|
subscriptionStartDate: values.subscriptionStartDate
|
||||||
|
? moment(values.subscriptionStartDate, 'jYYYY/jMM/jDD').format('YYYY-MM-DD')
|
||||||
|
: undefined,
|
||||||
|
subscriptionEndDate: values.subscriptionEndDate
|
||||||
|
? moment(values.subscriptionEndDate, 'jYYYY/jMM/jDD').format('YYYY-MM-DD')
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
updateShop.mutate(
|
||||||
|
{ id, params },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t('success'))
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['shops'] })
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['shop', id] })
|
||||||
|
navigate(Pages.dkala.shops.list)
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error?.message?.[0] || 'خطا در بروزرسانی فروشگاه')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shop) {
|
||||||
|
formik.setValues({
|
||||||
|
name: shop.name || undefined,
|
||||||
|
slug: shop.slug || undefined,
|
||||||
|
establishedYear: shop.establishedYear || undefined,
|
||||||
|
phone: shop.phone || undefined,
|
||||||
|
plan: shop.plan || undefined,
|
||||||
|
subscriptionId: shop.subscriptionId || undefined,
|
||||||
|
subscriptionEndDate: shop.subscriptionEndDate || undefined,
|
||||||
|
subscriptionStartDate: shop.subscriptionStartDate || undefined,
|
||||||
|
isActive: shop.isActive,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [shop])
|
||||||
|
|
||||||
|
const planOptions = [
|
||||||
|
{ value: 'base', label: 'پایه' },
|
||||||
|
{ value: 'premium', label: 'پرمیوم' },
|
||||||
|
]
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <PageLoading />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full mt-4'>
|
||||||
|
<div className='flex w-full justify-between items-center'>
|
||||||
|
<div>
|
||||||
|
{t('shop.list_shop')}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className='px-5'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={updateShop.isPending}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<TickCircle
|
||||||
|
className='size-5'
|
||||||
|
color='#fff'
|
||||||
|
/>
|
||||||
|
<div>{t('save')}</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-6 mt-10'>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||||
|
<div className='rowTwoInput'>
|
||||||
|
<Input
|
||||||
|
label={t('shop.name')}
|
||||||
|
{...formik.getFieldProps('name')}
|
||||||
|
error_text={formik.touched.name && formik.errors.name ? formik.errors.name : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label={t('shop.slug')}
|
||||||
|
{...formik.getFieldProps('slug')}
|
||||||
|
error_text={formik.touched.slug && formik.errors.slug ? formik.errors.slug : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-8 rowTwoInput'>
|
||||||
|
<Input
|
||||||
|
label={t('shop.phone')}
|
||||||
|
{...formik.getFieldProps('phone')}
|
||||||
|
error_text={formik.touched.phone && formik.errors.phone ? formik.errors.phone : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label={t('shop.established_year')}
|
||||||
|
type='number'
|
||||||
|
{...formik.getFieldProps('establishedYear')}
|
||||||
|
error_text={formik.touched.establishedYear && formik.errors.establishedYear ? formik.errors.establishedYear : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-8 rowTwoInput'>
|
||||||
|
<Select
|
||||||
|
label={t('shop.plan')}
|
||||||
|
items={planOptions}
|
||||||
|
{...formik.getFieldProps('plan')}
|
||||||
|
placeholder={t('select')}
|
||||||
|
error_text={formik.touched.plan && formik.errors.plan ? formik.errors.plan : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label={t('shop.subscription_id')}
|
||||||
|
{...formik.getFieldProps('subscriptionId')}
|
||||||
|
error_text={formik.touched.subscriptionId && formik.errors.subscriptionId ? formik.errors.subscriptionId : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-8 rowTwoInput'>
|
||||||
|
<DatePickerComponent
|
||||||
|
label={t('shop.subscription_start_date')}
|
||||||
|
onChange={(value) => formik.setFieldValue('subscriptionStartDate', value)}
|
||||||
|
placeholder={t('select')}
|
||||||
|
defaulValue={formik.values.subscriptionStartDate}
|
||||||
|
error_text={formik.touched.subscriptionStartDate && formik.errors.subscriptionStartDate ? formik.errors.subscriptionStartDate : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DatePickerComponent
|
||||||
|
label={t('shop.subscription_end_date')}
|
||||||
|
onChange={(value) => formik.setFieldValue('subscriptionEndDate', value)}
|
||||||
|
placeholder={t('select')}
|
||||||
|
defaulValue={formik.values.subscriptionEndDate}
|
||||||
|
error_text={formik.touched.subscriptionEndDate && formik.errors.subscriptionEndDate ? formik.errors.subscriptionEndDate : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-8'>
|
||||||
|
<SwitchComponent
|
||||||
|
label={t('shop.status')}
|
||||||
|
active={formik.values.isActive || false}
|
||||||
|
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdateShop
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { AddAdminShopType } from '../../types/Types'
|
||||||
|
import Input from '../../../../components/Input'
|
||||||
|
import Select from '../../../../components/Select'
|
||||||
|
import Button from '../../../../components/Button'
|
||||||
|
import { useAddAdminShop, useGetSystemRoles } from '../../hooks/useIconData'
|
||||||
|
import { SystemRoleType } from '../../types/Types'
|
||||||
|
import { ErrorType } from '../../../../helpers/types'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { TickCircle } from 'iconsax-react'
|
||||||
|
|
||||||
|
interface AddShopAdminFormProps {
|
||||||
|
shopId: string
|
||||||
|
onSuccess: () => void
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddShopAdminForm: FC<AddShopAdminFormProps> = ({
|
||||||
|
shopId,
|
||||||
|
onSuccess,
|
||||||
|
onClose
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const { mutate: addAdmin, isPending } = useAddAdminShop()
|
||||||
|
const { data: rolesData } = useGetSystemRoles()
|
||||||
|
|
||||||
|
const rolesList = rolesData?.data || []
|
||||||
|
|
||||||
|
const formik = useFormik<AddAdminShopType>({
|
||||||
|
initialValues: {
|
||||||
|
shopId: shopId,
|
||||||
|
phone: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
roleId: ''
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
phone: Yup.string().required(t('errors.required')),
|
||||||
|
firstName: Yup.string().required(t('errors.required')),
|
||||||
|
lastName: Yup.string().required(t('errors.required')),
|
||||||
|
roleId: Yup.string().required(t('errors.required'))
|
||||||
|
}),
|
||||||
|
onSubmit: (values) => {
|
||||||
|
addAdmin(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t('success'))
|
||||||
|
formik.resetForm()
|
||||||
|
onSuccess()
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error?.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={formik.handleSubmit} className='mt-6'>
|
||||||
|
<div className='flex flex-col gap-6'>
|
||||||
|
<div className='rowTwoInput'>
|
||||||
|
<Input
|
||||||
|
label={t('user.name')}
|
||||||
|
{...formik.getFieldProps('firstName')}
|
||||||
|
error_text={formik.touched.firstName && formik.errors.firstName ? formik.errors.firstName : ''}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label={t('user.family')}
|
||||||
|
{...formik.getFieldProps('lastName')}
|
||||||
|
error_text={formik.touched.lastName && formik.errors.lastName ? formik.errors.lastName : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='rowTwoInput'>
|
||||||
|
<Input
|
||||||
|
label={t('shop.phone')}
|
||||||
|
{...formik.getFieldProps('phone')}
|
||||||
|
error_text={formik.touched.phone && formik.errors.phone ? formik.errors.phone : ''}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label={t('user.role')}
|
||||||
|
placeholder={t('select')}
|
||||||
|
items={rolesList.map((item: SystemRoleType) => ({
|
||||||
|
value: item.id,
|
||||||
|
label: item.name
|
||||||
|
}))}
|
||||||
|
{...formik.getFieldProps('roleId')}
|
||||||
|
error_text={formik.touched.roleId && formik.errors.roleId ? formik.errors.roleId : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-4 justify-end mt-4'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
onClick={onClose}
|
||||||
|
className='px-6 bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||||
|
>
|
||||||
|
{t('cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
isLoading={isPending}
|
||||||
|
className='px-6'
|
||||||
|
>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<TickCircle
|
||||||
|
className='size-5'
|
||||||
|
color='white'
|
||||||
|
/>
|
||||||
|
<div className='text-sm'>
|
||||||
|
{t('shop.add_admin')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddShopAdminForm
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import DefaulModal from '../../../../components/DefaulModal'
|
||||||
|
import { useDeleteShopAdmin, useGetShopAdmins } from '../../hooks/useIconData'
|
||||||
|
import PageLoading from '../../../../components/PageLoading'
|
||||||
|
import Td from '../../../../components/Td'
|
||||||
|
import { ShopAdminType } from '../../types/Types'
|
||||||
|
import moment from 'moment-jalaali'
|
||||||
|
import TrashWithConfrim from '../../../../components/TrashWithConfrim'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { ErrorType } from '../../../../helpers/types'
|
||||||
|
import Button from '../../../../components/Button'
|
||||||
|
import AddShopAdminForm from './AddShopAdminForm'
|
||||||
|
|
||||||
|
interface ShopAdminsModalProps {
|
||||||
|
open: boolean
|
||||||
|
close: () => void
|
||||||
|
shopId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShopAdminsModal: FC<ShopAdminsModalProps> = ({ open, close, shopId }) => {
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const { data: adminsData, isLoading, refetch } = useGetShopAdmins(shopId)
|
||||||
|
const [showAddForm, setShowAddForm] = useState(false)
|
||||||
|
|
||||||
|
const adminsList = adminsData?.data || []
|
||||||
|
|
||||||
|
const { mutate: deleteAdmin, isPending } = useDeleteShopAdmin()
|
||||||
|
|
||||||
|
const handleDeleteAdmin = (shopId: string, adminId: string) => {
|
||||||
|
deleteAdmin({ shopId, adminId }, {
|
||||||
|
onSuccess: () => {
|
||||||
|
refetch()
|
||||||
|
toast.success(t('success'))
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error?.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddAdmin = () => {
|
||||||
|
setShowAddForm(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFormSuccess = () => {
|
||||||
|
refetch()
|
||||||
|
setShowAddForm(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFormClose = () => {
|
||||||
|
setShowAddForm(false)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<DefaulModal
|
||||||
|
open={open}
|
||||||
|
close={close}
|
||||||
|
title_header={t('shop.admins')}
|
||||||
|
isHeader
|
||||||
|
>
|
||||||
|
<div className='mt-6'>
|
||||||
|
{showAddForm ? (
|
||||||
|
<AddShopAdminForm
|
||||||
|
shopId={shopId}
|
||||||
|
onSuccess={handleFormSuccess}
|
||||||
|
onClose={handleFormClose}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className='flex justify-end'>
|
||||||
|
<Button
|
||||||
|
onClick={handleAddAdmin}
|
||||||
|
className='w-fit px-5'
|
||||||
|
>
|
||||||
|
{t('shop.add_admin')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
isLoading ?
|
||||||
|
<PageLoading />
|
||||||
|
:
|
||||||
|
<div className='relative overflow-x-auto rounded-3xl w-full mt-6'>
|
||||||
|
<table className='w-full text-sm'>
|
||||||
|
<thead className='thead'>
|
||||||
|
<tr>
|
||||||
|
<Td text={t('user.name')} />
|
||||||
|
<Td text={t('shop.phone')} />
|
||||||
|
<Td text={t('shop.role')} />
|
||||||
|
<Td text={t('shop.created_at')} />
|
||||||
|
<Td text={''} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
adminsList.map((admin: ShopAdminType) => {
|
||||||
|
return (
|
||||||
|
<tr key={admin.id} className='tr'>
|
||||||
|
<Td text={`${admin.firstName} ${admin.lastName}`} />
|
||||||
|
<Td text={admin.phone || '-'} />
|
||||||
|
<Td text={admin.roles[0].role?.name || '-'} />
|
||||||
|
<Td text={''}>
|
||||||
|
<div className='dltr text-right'>
|
||||||
|
{moment(admin.createdAt).format('jYYYY-jMM-jDD HH:mm')}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text=''>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<TrashWithConfrim
|
||||||
|
onDelete={() => handleDeleteAdmin(shopId, admin.id)}
|
||||||
|
isLoading={isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DefaulModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShopAdminsModal
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
import { IResponse } from "../../../types/response.types";
|
||||||
|
|
||||||
|
export type CreateGroupIconType = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GroupIconType = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
icons: unknown[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface GroupIconsResponse extends IResponse<GroupIconType[]> {
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateIconType = {
|
||||||
|
url: string;
|
||||||
|
groupId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IconGroupType = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IconType = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
url: string;
|
||||||
|
group: IconGroupType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IconsResponse extends IResponse<IconType[]> {
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReportItem = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
subject: string;
|
||||||
|
content: string;
|
||||||
|
phone: string | null;
|
||||||
|
scope: string;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ReportsResponse extends IResponse<ReportItem[]> {
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScoreType = {
|
||||||
|
scoreAmount: string;
|
||||||
|
scoreCredit: string;
|
||||||
|
birthdayScore: string;
|
||||||
|
purchaseScore: string;
|
||||||
|
referrerScore: string;
|
||||||
|
registerScore: string;
|
||||||
|
purchaseAmount: string;
|
||||||
|
marriageDateScore: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShopType = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
logo: string | null;
|
||||||
|
address: string | null;
|
||||||
|
menuColor: string | null;
|
||||||
|
latitude: number | null;
|
||||||
|
longitude: number | null;
|
||||||
|
serviceArea: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
establishedYear: number;
|
||||||
|
phone: string;
|
||||||
|
instagram: string | null;
|
||||||
|
telegram: string | null;
|
||||||
|
whatsapp: string | null;
|
||||||
|
description: string | null;
|
||||||
|
seoTitle: string | null;
|
||||||
|
seoDescription: string | null;
|
||||||
|
tagNames: string | null;
|
||||||
|
images: string | null;
|
||||||
|
vat: number;
|
||||||
|
domain: string;
|
||||||
|
score: ScoreType | null;
|
||||||
|
plan: string;
|
||||||
|
subscriptionId: string;
|
||||||
|
subscriptionEndDate: string;
|
||||||
|
subscriptionStartDate: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PagerType = {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
prevPage: boolean;
|
||||||
|
nextPage: string | false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShopsData = {
|
||||||
|
pager: PagerType;
|
||||||
|
shops: ShopType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ShopsResponse {
|
||||||
|
statusCode: number;
|
||||||
|
success: boolean;
|
||||||
|
data: ShopsData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShopResponse extends IResponse<ShopType> {
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShopAdminRoleType = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
isSystem: boolean;
|
||||||
|
shop: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShopAdminRoleItemType = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
admin: string;
|
||||||
|
role: ShopAdminRoleType;
|
||||||
|
shop: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShopAdminType = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
phone: string;
|
||||||
|
roles: ShopAdminRoleItemType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ShopAdminsResponse
|
||||||
|
extends IResponse<ShopAdminType[]> {
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShopSmsCountItem = {
|
||||||
|
shopId: string;
|
||||||
|
shopName: string;
|
||||||
|
smsCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ShopSmsCountResponse
|
||||||
|
extends IResponse<ShopSmsCountItem[]> {
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AddAdminShopType = {
|
||||||
|
shopId: string;
|
||||||
|
phone: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
roleId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PermissionType = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SystemRoleType = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
isSystem: boolean;
|
||||||
|
shop: string | null;
|
||||||
|
permissions: PermissionType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface SystemRolesResponse extends IResponse<SystemRoleType[]> {
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateShopType = {
|
||||||
|
name?: string;
|
||||||
|
slug?: string;
|
||||||
|
establishedYear?: number;
|
||||||
|
phone?: string;
|
||||||
|
plan?: string;
|
||||||
|
subscriptionId?: string;
|
||||||
|
subscriptionEndDate?: string;
|
||||||
|
subscriptionStartDate?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
@@ -83,6 +83,12 @@ import WarningsList from '../pages/dmenu/report/List.tsx'
|
|||||||
import RestaurantsList from '../pages/dmenu/restaurant/List.tsx'
|
import RestaurantsList from '../pages/dmenu/restaurant/List.tsx'
|
||||||
import SmsCountList from '../pages/dmenu/restaurant/SmsCountList.tsx'
|
import SmsCountList from '../pages/dmenu/restaurant/SmsCountList.tsx'
|
||||||
import UpdateRestaurant from '../pages/dmenu/restaurant/Update.tsx'
|
import UpdateRestaurant from '../pages/dmenu/restaurant/Update.tsx'
|
||||||
|
import DkalaIconsList from '../pages/dkala/icon/List'
|
||||||
|
import DkalaGroupIconList from '../pages/dkala/icon/GroupList'
|
||||||
|
import DkalaWarningsList from '../pages/dkala/report/List.tsx'
|
||||||
|
import ShopsList from '../pages/dkala/shop/List.tsx'
|
||||||
|
import DkalaSmsCountList from '../pages/dkala/shop/SmsCountList.tsx'
|
||||||
|
import UpdateShop from '../pages/dkala/shop/Update.tsx'
|
||||||
import SubscriptionsList from '../pages/subscriptions/List.tsx'
|
import SubscriptionsList from '../pages/subscriptions/List.tsx'
|
||||||
// import WarningsList from '../pages/dmenu/reports/List' // TODO: Create this component
|
// import WarningsList from '../pages/dmenu/reports/List' // TODO: Create this component
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
@@ -181,6 +187,13 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Pages.dmenu.restaurants.smsCountList} element={<SmsCountList />} />
|
<Route path={Pages.dmenu.restaurants.smsCountList} element={<SmsCountList />} />
|
||||||
<Route path={Pages.dmenu.restaurants.update + ':id'} element={<UpdateRestaurant />} />
|
<Route path={Pages.dmenu.restaurants.update + ':id'} element={<UpdateRestaurant />} />
|
||||||
|
|
||||||
|
<Route path={Pages.dkala.icons.list} element={<DkalaIconsList />} />
|
||||||
|
<Route path={Pages.dkala.icons.groupList} element={<DkalaGroupIconList />} />
|
||||||
|
<Route path={Pages.dkala.warnings.list} element={<DkalaWarningsList />} />
|
||||||
|
<Route path={Pages.dkala.shops.list} element={<ShopsList />} />
|
||||||
|
<Route path={Pages.dkala.shops.smsCountList} element={<DkalaSmsCountList />} />
|
||||||
|
<Route path={Pages.dkala.shops.update + ':id'} element={<UpdateShop />} />
|
||||||
|
|
||||||
<Route path={Pages.subscriptions.list} element={<SubscriptionsList />} />
|
<Route path={Pages.subscriptions.list} element={<SubscriptionsList />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -366,6 +366,70 @@ const SideBar: FC = () => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
adminPermissions?.data?.permissions?.some((permission: { name: string }) => permission.name === 'dkala') &&
|
||||||
|
<>
|
||||||
|
<div className={clx(
|
||||||
|
'mt-10 px-12 text-header text-sm font-normal',
|
||||||
|
hasSubMenu && 'px-2 text-center'
|
||||||
|
)}>
|
||||||
|
{t('sidebar.dkala')}
|
||||||
|
</div>
|
||||||
|
<div className='flex-1 flex flex-col justify-end'>
|
||||||
|
|
||||||
|
<div className='text-xs text-[#8C90A3]'>
|
||||||
|
<SideBarItem
|
||||||
|
icon={<Icon variant={isActive('dkala/icons') ? 'Bold' : 'Outline'} color={isActive('dkala/icons') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||||
|
title={t('sidebar.icons')}
|
||||||
|
isActive={isActive('dkala/icons')}
|
||||||
|
link={Pages.dkala.icons.list}
|
||||||
|
activeName='dkala'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='text-xs text-[#8C90A3]'>
|
||||||
|
<SideBarItem
|
||||||
|
icon={<Category variant={isActive('dkala/group') ? 'Bold' : 'Outline'} color={isActive('dkala/group') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||||
|
title={t('sidebar.groups_icon')}
|
||||||
|
isActive={isActive('dkala/icons/group')}
|
||||||
|
link={Pages.dkala.icons.groupList}
|
||||||
|
activeName='dkala'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='text-xs text-[#8C90A3]'>
|
||||||
|
<SideBarItem
|
||||||
|
icon={<Warning2 variant={isActive('dkala/reports') ? 'Bold' : 'Outline'} color={isActive('dkala/reports') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||||
|
title={t('sidebar.reports')}
|
||||||
|
isActive={isActive('dkala/reports')}
|
||||||
|
link={Pages.dkala.warnings.list}
|
||||||
|
activeName='dkala'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='text-xs text-[#8C90A3]'>
|
||||||
|
<SideBarItem
|
||||||
|
icon={<Building variant={isActive('dkala/shops') ? 'Bold' : 'Outline'} color={isActive('dkala/shops') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||||
|
title={t('sidebar.shops')}
|
||||||
|
isActive={isActive('dkala/shops')}
|
||||||
|
link={Pages.dkala.shops.list}
|
||||||
|
activeName='dkala'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='text-xs text-[#8C90A3]'>
|
||||||
|
<SideBarItem
|
||||||
|
icon={<Sms variant={isActive('dkala/sms-count') ? 'Bold' : 'Outline'} color={isActive('dkala/sms-count') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||||
|
title={t('sidebar.sms_count')}
|
||||||
|
isActive={isActive('dkala/sms-count')}
|
||||||
|
link={Pages.dkala.shops.smsCountList}
|
||||||
|
activeName='dkala'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
<div className='flex-1 flex flex-col justify-end mt-14'>
|
<div className='flex-1 flex flex-col justify-end mt-14'>
|
||||||
<div className='text-xs text-[#8C90A3]'>
|
<div className='text-xs text-[#8C90A3]'>
|
||||||
<SideBarItem
|
<SideBarItem
|
||||||
|
|||||||
Reference in New Issue
Block a user