@@ -10,3 +10,19 @@ export const useGetDashboardCounts = () => {
|
||||
refetchInterval: DASHBOARD_COUNTS_REFETCH_MS,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetHomeStats = () => {
|
||||
return useQuery({
|
||||
queryKey: ['home-stats'],
|
||||
queryFn: () => api.getHomeStats(),
|
||||
refetchInterval: DASHBOARD_COUNTS_REFETCH_MS,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetWeeklyOrders = () => {
|
||||
return useQuery({
|
||||
queryKey: ['weekly-orders'],
|
||||
queryFn: () => api.getWeeklyOrders(),
|
||||
refetchInterval: DASHBOARD_COUNTS_REFETCH_MS,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -8,7 +8,30 @@ export type DashboardCountsType = {
|
||||
paymentsCount: number;
|
||||
};
|
||||
|
||||
export type HomeStatsType = {
|
||||
requestsCount: number;
|
||||
unconfirmedInvoicesCount: number;
|
||||
inProgressOrdersCount: number;
|
||||
completedOrdersCount: number;
|
||||
customersCount: number;
|
||||
};
|
||||
|
||||
export type WeeklyOrdersPoint = {
|
||||
weekStart: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export const getDashboardCounts = async (): Promise<BaseResponse<DashboardCountsType>> => {
|
||||
const { data } = await axios.get<BaseResponse<DashboardCountsType>>('/admin/dashboard/counts');
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getHomeStats = async (): Promise<BaseResponse<HomeStatsType>> => {
|
||||
const { data } = await axios.get<BaseResponse<HomeStatsType>>('/admin/home/stats');
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getWeeklyOrders = async (): Promise<BaseResponse<WeeklyOrdersPoint[]>> => {
|
||||
const { data } = await axios.get<BaseResponse<WeeklyOrdersPoint[]>>('/admin/home/weekly-orders');
|
||||
return data;
|
||||
};
|
||||
|
||||
+6
-72
@@ -1,80 +1,14 @@
|
||||
import { type FC } from 'react'
|
||||
import Stats from './components/Stats'
|
||||
import Services from './components/Services'
|
||||
import Orders from './components/Orders'
|
||||
import NewOrderImage from '@/assets/images/new_order.png'
|
||||
import { t } from '@/locale'
|
||||
import Button from '@/components/Button'
|
||||
import { AddSquare, MessageQuestion } from 'iconsax-react'
|
||||
import { COLORS } from '@/constants/colors'
|
||||
import SupportImage from '@/assets/images/support1.png'
|
||||
import WeeklyOrdersChart from './components/WeeklyOrdersChart'
|
||||
|
||||
const Home: FC = () => {
|
||||
return (
|
||||
<div className='flex gap-6'>
|
||||
<div className='flex-1'>
|
||||
<Stats />
|
||||
<Services />
|
||||
<Orders />
|
||||
</div>
|
||||
<div className='min-w-[267px] mt-5'>
|
||||
<div className='bg-white rounded-3xl p-6'>
|
||||
<img src={NewOrderImage} className='w-[98px] mx-auto' />
|
||||
<h4 className='mt-4 text-center font-medium'>
|
||||
{t('home.submitNewOrder')}
|
||||
</h4>
|
||||
<Button
|
||||
className='mt-2.5'>
|
||||
<div className='flex gap-1'>
|
||||
<AddSquare size={20} color='#292D32' />
|
||||
<div className=''>{t('home.submitNewOrderButton')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='mt-7 bg-white rounded-3xl p-6 relative'>
|
||||
<h4>
|
||||
{t('home.specialSolution')}
|
||||
</h4>
|
||||
<p className='mt-2 text-[#7B7E8B] text-xs'>
|
||||
{t('home.specialSolutionDescription')}
|
||||
</p>
|
||||
|
||||
<div className='mt-2 flex flex-col gap-2'>
|
||||
<div className='flex gap-2 items-center text-xs'>
|
||||
<div className='size-1.5 rounded-full bg-primary'></div>
|
||||
<div>{t('home.specialSolutionFeature1')}</div>
|
||||
</div>
|
||||
<div className='flex gap-2 items-center text-xs'>
|
||||
<div className='size-1.5 rounded-full bg-primary'></div>
|
||||
<div>{t('home.specialSolutionFeature2')}</div>
|
||||
</div>
|
||||
<div className='flex gap-2 items-center text-xs'>
|
||||
<div className='size-1.5 rounded-full bg-primary'></div>
|
||||
<div>{t('home.specialSolutionFeature3')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
label={t('home.freeConsultation')}
|
||||
className='mt-12'
|
||||
/>
|
||||
|
||||
<div className='mt-2.5 bg-primary/11 py-6 px-4 rounded-[13px]'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<MessageQuestion size={20} color={COLORS.primary} />
|
||||
<div className='text-xs font-light'>{t('home.contactUs')}</div>
|
||||
</div>
|
||||
<div>{t('home.phoneNumber')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img src={SupportImage} className='w-[50px] absolute bottom-[159px] left-2' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<Stats />
|
||||
<WeeklyOrdersChart />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
export default Home
|
||||
|
||||
@@ -1,34 +1,40 @@
|
||||
import { COLORS } from '@/constants/colors'
|
||||
import { ArrowLeft } from 'iconsax-react'
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
type Props = {
|
||||
icon: ReactNode,
|
||||
count: number,
|
||||
icon: ReactNode
|
||||
count: number | undefined
|
||||
description: string
|
||||
to: string
|
||||
}
|
||||
|
||||
const StatCard: FC<Props> = (props) => {
|
||||
const { icon, count, description } = props
|
||||
const StatCard: FC<Props> = ({ icon, count, description, to }) => {
|
||||
return (
|
||||
<div className='flex-1 bg-white rounded-3xl p-6'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<Link
|
||||
to={to}
|
||||
className='block flex-1 rounded-3xl bg-white p-6 transition-shadow hover:shadow-md'
|
||||
>
|
||||
<div className='flex items-center justify-between'>
|
||||
{icon}
|
||||
|
||||
<div className='size-8 bg-[#FFF1D7] rounded-full flex justify-center items-center'>
|
||||
<ArrowLeft size={16} color={COLORS.primary} className='rotate-45' />
|
||||
<div className='flex size-8 items-center justify-center rounded-full bg-[#FFF1D7]'>
|
||||
<ArrowLeft
|
||||
size={16}
|
||||
color={COLORS.primary}
|
||||
className='rotate-45'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-2 text-2xl font-semibold'>
|
||||
{count}
|
||||
{count ?? 0}
|
||||
</div>
|
||||
|
||||
<div className='mt-2 text-sm'>
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-2 text-sm'>{description}</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatCard
|
||||
export default StatCard
|
||||
|
||||
@@ -1,53 +1,73 @@
|
||||
import GridWrapper from '@/components/GridWrapper'
|
||||
import { Paths } from '@/config/Paths'
|
||||
import { COLORS } from '@/constants/colors'
|
||||
import { useGetHomeStats } from '@/pages/dashboard/hooks/useDashboardData'
|
||||
import { ProformaInvoiceStatusEnum } from '@/pages/invoice/enum/InvoiceEnum'
|
||||
import { TabOrderListEnum } from '@/pages/order/enum/OrderEnum'
|
||||
import { t } from '@/locale'
|
||||
import { BoxTick, BoxTime, ReceiptText, TruckTick } from 'iconsax-react'
|
||||
import { BoxTick, BoxTime, People, ReceiptText, TruckTick } from 'iconsax-react'
|
||||
import { type FC } from 'react'
|
||||
import StatCard from './StatCard'
|
||||
|
||||
const Stats: FC = () => {
|
||||
const { data } = useGetHomeStats()
|
||||
const stats = data?.data
|
||||
|
||||
return (
|
||||
<GridWrapper
|
||||
desktop={4}
|
||||
desktop={5}
|
||||
mobile={2}
|
||||
gapDesktop={24}
|
||||
gapMobile={12}
|
||||
className='mt-5'
|
||||
>
|
||||
<StatCard
|
||||
count={2}
|
||||
count={stats?.requestsCount}
|
||||
description={t('home.requestCount')}
|
||||
to={Paths.requests.list}
|
||||
icon={<BoxTime
|
||||
size={27}
|
||||
color={COLORS.primary}
|
||||
/>}
|
||||
/>
|
||||
<StatCard
|
||||
count={2}
|
||||
count={stats?.unconfirmedInvoicesCount}
|
||||
description={t('home.factureCount')}
|
||||
to={`${Paths.perfomaInvoice.list}?tab=${ProformaInvoiceStatusEnum.NOT_CONFIRMED}`}
|
||||
icon={<ReceiptText
|
||||
size={27}
|
||||
color={COLORS.primary}
|
||||
/>}
|
||||
/>
|
||||
<StatCard
|
||||
count={10}
|
||||
count={stats?.inProgressOrdersCount}
|
||||
description={t('home.orderCount')}
|
||||
to={`${Paths.order.list}?tab=${TabOrderListEnum.IN_PROGRESS}`}
|
||||
icon={<BoxTick
|
||||
size={27}
|
||||
color={COLORS.primary}
|
||||
/>}
|
||||
/>
|
||||
<StatCard
|
||||
count={20}
|
||||
count={stats?.completedOrdersCount}
|
||||
description={t('home.orderDoneCount')}
|
||||
to={`${Paths.order.list}?tab=${TabOrderListEnum.FINISHED}`}
|
||||
icon={<TruckTick
|
||||
size={27}
|
||||
color={COLORS.primary}
|
||||
/>}
|
||||
/>
|
||||
<StatCard
|
||||
count={stats?.customersCount}
|
||||
description={t('home.customerCount')}
|
||||
to={Paths.users.list}
|
||||
icon={<People
|
||||
size={27}
|
||||
color={COLORS.primary}
|
||||
/>}
|
||||
/>
|
||||
</GridWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default Stats
|
||||
export default Stats
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { COLORS } from '@/constants/colors'
|
||||
import { useGetWeeklyOrders } from '@/pages/dashboard/hooks/useDashboardData'
|
||||
import { t } from '@/locale'
|
||||
import moment from 'moment-jalaali'
|
||||
import { type FC, useMemo } from 'react'
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts'
|
||||
|
||||
const WeeklyOrdersChart: FC = () => {
|
||||
const { data, isLoading } = useGetWeeklyOrders()
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
const points = data?.data ?? []
|
||||
return [...points].reverse().map((point) => ({
|
||||
week: moment(point.weekStart).format('jD jMMMM'),
|
||||
count: point.count,
|
||||
}))
|
||||
}, [data?.data])
|
||||
|
||||
return (
|
||||
<div className='mt-6 bg-white p-6 rounded-3xl'>
|
||||
<h4 className='text-lg font-light mb-6'>
|
||||
{t('home.weeklyOrders')}
|
||||
</h4>
|
||||
|
||||
{isLoading ? (
|
||||
<div className='h-64 bg-gray-100 animate-pulse rounded-2xl' />
|
||||
) : (
|
||||
<div className='h-64' dir='ltr'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<BarChart data={chartData} margin={{ top: 8, right: 8, left: -16, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray='3 3' vertical={false} stroke='#f0f0f0' />
|
||||
<XAxis
|
||||
dataKey='week'
|
||||
tick={{ fontSize: 12, fill: COLORS.grayLight }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
allowDecimals={false}
|
||||
tick={{ fontSize: 12, fill: COLORS.grayLight }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value) => [value, t('home.orders')]}
|
||||
labelFormatter={(label) => `${t('home.week')}: ${label}`}
|
||||
contentStyle={{
|
||||
borderRadius: 12,
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
|
||||
direction: 'rtl',
|
||||
}}
|
||||
/>
|
||||
<Bar
|
||||
dataKey='count'
|
||||
fill={COLORS.primary}
|
||||
radius={[8, 8, 0, 0]}
|
||||
maxBarSize={48}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WeeklyOrdersChart
|
||||
@@ -5,18 +5,38 @@ import Filters from '@/components/Filters'
|
||||
import RefreshButton from '@/components/RefreshButton'
|
||||
import Table from '@/components/Table'
|
||||
import { Eye, Add, Edit2 } from 'iconsax-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Link, useSearchParams } from 'react-router-dom'
|
||||
import { Paths } from '@/config/Paths'
|
||||
import moment from 'moment-jalaali'
|
||||
import type { FilterValues } from '@/components/Filters'
|
||||
import { useGetInvoice } from './hooks/useInvoiceData'
|
||||
import type { InvoiceType } from './types/Types'
|
||||
|
||||
const isValidInvoiceTab = (value: string | null): value is ProformaInvoiceStatusEnum =>
|
||||
Object.values(ProformaInvoiceStatusEnum).includes(value as ProformaInvoiceStatusEnum)
|
||||
|
||||
const ProformaInvoice: FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<ProformaInvoiceStatusEnum>(ProformaInvoiceStatusEnum.ALL)
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const tabParam = searchParams.get('tab')
|
||||
const activeTab = isValidInvoiceTab(tabParam) ? tabParam : ProformaInvoiceStatusEnum.ALL
|
||||
const [page, setPage] = useState(1)
|
||||
const [filters, setFilters] = useState<FilterValues>({})
|
||||
|
||||
const setActiveTab = (tab: ProformaInvoiceStatusEnum) => {
|
||||
setSearchParams(
|
||||
(current) => {
|
||||
const next = new URLSearchParams(current)
|
||||
if (tab === ProformaInvoiceStatusEnum.ALL) {
|
||||
next.delete('tab')
|
||||
} else {
|
||||
next.set('tab', tab)
|
||||
}
|
||||
return next
|
||||
},
|
||||
{ replace: true },
|
||||
)
|
||||
}
|
||||
|
||||
const { data, isLoading, refetch, isFetching } = useGetInvoice(page)
|
||||
|
||||
const meta = data?.meta
|
||||
|
||||
Reference in New Issue
Block a user