dkala ducatated from dmenu

This commit is contained in:
hamid zarghami
2026-02-15 15:03:19 +03:30
parent 32e7dcc0f4
commit 71053d8d45
19 changed files with 1919 additions and 1 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
VITE_TOKEN_NAME = 'admin_token'
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
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'
+14
View File
@@ -136,6 +136,20 @@ export const Pages = {
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: {
list: "/subscriptions/list",
},
+2
View File
@@ -8,4 +8,6 @@ export const SideBarItemHasSubMenu = [
"blog",
"support",
"accessLogs",
"dmenu",
"dkala",
];
+25
View File
@@ -76,9 +76,11 @@
"accessLogs": "لاگ‌های دسترسی",
"icons": "آیکون ها",
"dmenu": "دی منو",
"dkala": "دی کالا",
"groups_icon": "گروه های آیکون",
"reports": "گزارش ها",
"restaurants": "رستوران ها",
"shops": "فروشگاه ها",
"sms_count": "تعداد پیامک",
"subscriptions": "اشتراک ها"
},
@@ -895,6 +897,29 @@
"subscription_end_date": "تاریخ پایان اشتراک",
"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": {
"list_subscriptions": "لیست اشتراک‌ها",
"service": "سرویس",
+127
View File
@@ -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),
});
};
+125
View File
@@ -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
+144
View File
@@ -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
+144
View File
@@ -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
+121
View File
@@ -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;
};
+175
View File
@@ -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
+53
View File
@@ -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
+202
View File
@@ -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
+219
View File
@@ -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;
};
+13
View File
@@ -83,6 +83,12 @@ import WarningsList from '../pages/dmenu/report/List.tsx'
import RestaurantsList from '../pages/dmenu/restaurant/List.tsx'
import SmsCountList from '../pages/dmenu/restaurant/SmsCountList.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 WarningsList from '../pages/dmenu/reports/List' // TODO: Create this component
const MainRouter: FC = () => {
@@ -181,6 +187,13 @@ const MainRouter: FC = () => {
<Route path={Pages.dmenu.restaurants.smsCountList} element={<SmsCountList />} />
<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 />} />
</Routes>
</div>
+64
View File
@@ -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='text-xs text-[#8C90A3]'>
<SideBarItem