This commit is contained in:
2026-07-14 21:04:39 +03:30
parent 9aed33d73b
commit fa83d7d092
13 changed files with 116 additions and 228 deletions
-4
View File
@@ -938,9 +938,6 @@
"close_shift": "بستن شیفت", "close_shift": "بستن شیفت",
"open_title": "باز کردن شیفت صندوق", "open_title": "باز کردن شیفت صندوق",
"close_title": "بستن شیفت صندوق", "close_title": "بستن شیفت صندوق",
"edit_title": "ویرایش شیفت صندوق",
"edit_shift": "ویرایش شیفت",
"save_changes": "ذخیره تغییرات",
"detail_title": "جزئیات شیفت صندوق", "detail_title": "جزئیات شیفت صندوق",
"summary_title": "خلاصه شیفت", "summary_title": "خلاصه شیفت",
"operator": "اپراتور", "operator": "اپراتور",
@@ -962,7 +959,6 @@
"view_details": "مشاهده جزئیات", "view_details": "مشاهده جزئیات",
"opened_success": "شیفت صندوق با موفقیت باز شد", "opened_success": "شیفت صندوق با موفقیت باز شد",
"closed_success": "شیفت صندوق با موفقیت بسته شد", "closed_success": "شیفت صندوق با موفقیت بسته شد",
"updated_success": "شیفت صندوق با موفقیت ویرایش شد",
"status_values": { "status_values": {
"open": "باز", "open": "باز",
"closed": "بسته" "closed": "بسته"
+1 -28
View File
@@ -1,6 +1,6 @@
import { type FC, useMemo, useState } from "react"; import { type FC, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Add, Edit, Lock1 } from "iconsax-react"; import { Add, Lock1 } from "iconsax-react";
import Table from "@/components/Table"; import Table from "@/components/Table";
import Filters from "@/components/Filters"; import Filters from "@/components/Filters";
import Button from "@/components/Button"; import Button from "@/components/Button";
@@ -16,7 +16,6 @@ import { useCashShiftFiltersFields } from "./components/CashShiftFiltersFields";
import CashShiftSummaryCard from "./components/CashShiftSummaryCard"; import CashShiftSummaryCard from "./components/CashShiftSummaryCard";
import OpenCashShiftModal from "./components/OpenCashShiftModal"; import OpenCashShiftModal from "./components/OpenCashShiftModal";
import CloseCashShiftModal from "./components/CloseCashShiftModal"; import CloseCashShiftModal from "./components/CloseCashShiftModal";
import EditCashShiftModal from "./components/EditCashShiftModal";
import CashShiftDetailModal from "./components/CashShiftDetailModal"; import CashShiftDetailModal from "./components/CashShiftDetailModal";
import type { CashShift } from "./types/Types"; import type { CashShift } from "./types/Types";
import { CashShiftStatus } from "./enum/Enum"; import { CashShiftStatus } from "./enum/Enum";
@@ -28,7 +27,6 @@ const CashShiftsList: FC = () => {
const { t } = useTranslation("global"); const { t } = useTranslation("global");
const [isOpenModalVisible, setIsOpenModalVisible] = useState(false); const [isOpenModalVisible, setIsOpenModalVisible] = useState(false);
const [isCloseModalVisible, setIsCloseModalVisible] = useState(false); const [isCloseModalVisible, setIsCloseModalVisible] = useState(false);
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false); const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [selectedShift, setSelectedShift] = useState<CashShift | null>(null); const [selectedShift, setSelectedShift] = useState<CashShift | null>(null);
@@ -61,10 +59,6 @@ const CashShiftsList: FC = () => {
setSelectedShift(shift); setSelectedShift(shift);
setIsDetailModalVisible(true); setIsDetailModalVisible(true);
}, },
onEdit: (shift) => {
setSelectedShift(shift);
setIsEditModalVisible(true);
},
onClose: (shift) => { onClose: (shift) => {
setSelectedShift(shift); setSelectedShift(shift);
setIsCloseModalVisible(true); setIsCloseModalVisible(true);
@@ -129,18 +123,6 @@ const CashShiftsList: FC = () => {
variant="warning" variant="warning"
label={t(`cash_shift.status_values.${CashShiftStatus.OPEN}`)} label={t(`cash_shift.status_values.${CashShiftStatus.OPEN}`)}
/> />
<Button
className="w-fit px-5 bg-gray-100 text-gray-700"
onClick={() => {
setSelectedShift(currentShift);
setIsEditModalVisible(true);
}}
>
<div className="flex gap-2 items-center">
<Edit color="#374151" size={18} />
<span>{t("cash_shift.edit_shift")}</span>
</div>
</Button>
<Button <Button
className="w-fit px-5" className="w-fit px-5"
onClick={() => { onClick={() => {
@@ -199,15 +181,6 @@ const CashShiftsList: FC = () => {
shift={selectedShift} shift={selectedShift}
/> />
<EditCashShiftModal
open={isEditModalVisible}
onClose={() => {
setIsEditModalVisible(false);
setSelectedShift(null);
}}
shift={selectedShift}
/>
<CashShiftDetailModal <CashShiftDetailModal
open={isDetailModalVisible} open={isDetailModalVisible}
onClose={() => { onClose={() => {
@@ -1,4 +1,4 @@
import { Edit, Eye, Lock1 } from "iconsax-react"; import { Eye, Lock1 } from "iconsax-react";
import type { ColumnType } from "@/components/types/TableTypes"; import type { ColumnType } from "@/components/types/TableTypes";
import type { CashShift } from "../types/Types"; import type { CashShift } from "../types/Types";
import { import {
@@ -13,7 +13,6 @@ import type { TFunction } from "i18next";
interface GetCashShiftTableColumnsParams { interface GetCashShiftTableColumnsParams {
t: TFunction; t: TFunction;
onView: (shift: CashShift) => void; onView: (shift: CashShift) => void;
onEdit?: (shift: CashShift) => void;
onClose?: (shift: CashShift) => void; onClose?: (shift: CashShift) => void;
} }
@@ -27,7 +26,6 @@ const getAdminName = (shift: CashShift) => {
export const getCashShiftTableColumns = ({ export const getCashShiftTableColumns = ({
t, t,
onView, onView,
onEdit,
onClose, onClose,
}: GetCashShiftTableColumnsParams): ColumnType<CashShift>[] => [ }: GetCashShiftTableColumnsParams): ColumnType<CashShift>[] => [
{ {
@@ -112,15 +110,6 @@ export const getCashShiftTableColumns = ({
> >
<Eye size={20} color="#8C90A3" /> <Eye size={20} color="#8C90A3" />
</button> </button>
{onEdit && (
<button
onClick={() => onEdit(item)}
className="cursor-pointer hover:opacity-70 transition-opacity"
title={t("cash_shift.edit_shift")}
>
<Edit size={20} color="#8C90A3" />
</button>
)}
{item.status === CashShiftStatus.OPEN && onClose && ( {item.status === CashShiftStatus.OPEN && onClose && (
<button <button
onClick={() => onClose(item)} onClick={() => onClose(item)}
@@ -1,125 +0,0 @@
import { type FC, useEffect, useState } from "react";
import DefaulModal from "@/components/DefaulModal";
import Input from "@/components/Input";
import Textarea from "@/components/Textarea";
import Button from "@/components/Button";
import { useTranslation } from "react-i18next";
import { toast } from "react-toastify";
import { extractErrorMessage } from "@/config/func";
import { useUpdateCashShift } from "../hooks/useCashShiftData";
import type { CashShift } from "../types/Types";
type Props = {
open: boolean;
onClose: () => void;
shift: CashShift | null;
};
const EditCashShiftModal: FC<Props> = ({ open, onClose, shift }) => {
const { t } = useTranslation("global");
const [openingAmount, setOpeningAmount] = useState("");
const [notes, setNotes] = useState("");
const [error, setError] = useState("");
const updateShift = useUpdateCashShift();
useEffect(() => {
if (open && shift) {
setOpeningAmount(String(shift.openingAmount));
setNotes(shift.notes ?? "");
setError("");
}
}, [open, shift]);
const handleClose = () => {
setOpeningAmount("");
setNotes("");
setError("");
onClose();
};
const handleSubmit = () => {
if (!shift) return;
const numericAmount = Number(openingAmount);
if (Number.isNaN(numericAmount) || numericAmount < 0) {
setError(t("cash_shift.opening_amount_required"));
return;
}
updateShift.mutate(
{
shiftId: shift.id,
payload: {
openingAmount: numericAmount,
notes: notes.trim() || undefined,
},
},
{
onSuccess: () => {
toast.success(t("cash_shift.updated_success"));
handleClose();
},
onError: (err) => {
toast.error(extractErrorMessage(err));
},
}
);
};
if (!shift) return null;
return (
<DefaulModal
open={open}
close={handleClose}
isHeader
title_header={t("cash_shift.edit_title")}
width={480}
>
<div className="mt-6">
<Input
name="openingAmount"
label={t("cash_shift.opening_amount")}
placeholder={t("cash_shift.opening_amount_placeholder")}
type="number"
seprator
value={openingAmount}
onChange={(e) => {
setOpeningAmount(e.target.value);
setError("");
}}
error_text={error}
/>
<div className="mt-4">
<Textarea
name="notes"
label={t("cash_shift.notes")}
placeholder={t("cash_shift.notes_placeholder")}
value={notes}
onChange={(e) => setNotes(e.target.value)}
isNotRequired
/>
</div>
<div className="flex gap-3 mt-6">
<Button
label={t("cash_shift.cancel")}
type="button"
onClick={handleClose}
className="bg-gray-100 text-gray-700"
/>
<Button
label={t("cash_shift.save_changes")}
type="button"
onClick={handleSubmit}
isloading={updateShift.isPending}
/>
</div>
</div>
</DefaulModal>
);
};
export default EditCashShiftModal;
@@ -3,7 +3,6 @@ import * as api from "../service/CashShiftService";
import type { import type {
CloseCashShiftPayload, CloseCashShiftPayload,
OpenCashShiftPayload, OpenCashShiftPayload,
UpdateCashShiftPayload,
} from "../types/Types"; } from "../types/Types";
import type { GetCashShiftsParams } from "../service/CashShiftService"; import type { GetCashShiftsParams } from "../service/CashShiftService";
@@ -65,17 +64,3 @@ export const useCloseCashShift = () => {
onSuccess: () => invalidateCashShiftQueries(queryClient), onSuccess: () => invalidateCashShiftQueries(queryClient),
}); });
}; };
export const useUpdateCashShift = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
shiftId,
payload,
}: {
shiftId: string;
payload: UpdateCashShiftPayload;
}) => api.updateCashShift(shiftId, payload),
onSuccess: () => invalidateCashShiftQueries(queryClient),
});
};
@@ -5,7 +5,6 @@ import type {
GetCashShiftsResponse, GetCashShiftsResponse,
GetCashShiftSummaryResponse, GetCashShiftSummaryResponse,
OpenCashShiftPayload, OpenCashShiftPayload,
UpdateCashShiftPayload,
} from "../types/Types"; } from "../types/Types";
export interface GetCashShiftsParams { export interface GetCashShiftsParams {
@@ -81,14 +80,3 @@ export const closeCashShift = async (
); );
return data; return data;
}; };
export const updateCashShift = async (
shiftId: string,
payload: UpdateCashShiftPayload
): Promise<GetCashShiftResponse> => {
const { data } = await axios.patch<GetCashShiftResponse>(
`/admin/cash-shifts/${shiftId}`,
payload
);
return data;
};
-5
View File
@@ -58,8 +58,3 @@ export interface CloseCashShiftPayload {
countedAmount: number; countedAmount: number;
notes?: string; notes?: string;
} }
export interface UpdateCashShiftPayload {
openingAmount?: number;
notes?: string;
}
+16
View File
@@ -4,6 +4,9 @@ import { DocumentDownload } from 'iconsax-react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import Filters from '@/components/Filters'; import Filters from '@/components/Filters';
import Button from '@/components/Button'; import Button from '@/components/Button';
import PageLoading from '@/components/PageLoading';
import { usePermissions } from '@/hooks/usePermissions';
import { PermissionEnum } from '@/pages/roles/enum/Enum';
import { useGetDailyOrderReport } from './hooks/useDailyOrderReportData'; import { useGetDailyOrderReport } from './hooks/useDailyOrderReportData';
import { useDailyOrderReportFilters } from './hooks/useDailyOrderReportFilters'; import { useDailyOrderReportFilters } from './hooks/useDailyOrderReportFilters';
import { getDailyOrderReportTableColumns } from './components/DailyOrderReportTableColumns'; import { getDailyOrderReportTableColumns } from './components/DailyOrderReportTableColumns';
@@ -15,6 +18,7 @@ import type { RowDataType } from '@/components/types/TableTypes';
const DailyOrderReport: FC = () => { const DailyOrderReport: FC = () => {
const { t } = useTranslation('global'); const { t } = useTranslation('global');
const { checkPermission, isLoading: isPermissionsLoading } = usePermissions();
const { filters, apiParams, handleFiltersChange } = useDailyOrderReportFilters(); const { filters, apiParams, handleFiltersChange } = useDailyOrderReportFilters();
const { data: reportData, isLoading } = useGetDailyOrderReport(apiParams); const { data: reportData, isLoading } = useGetDailyOrderReport(apiParams);
const [isExporting, setIsExporting] = useState(false); const [isExporting, setIsExporting] = useState(false);
@@ -43,6 +47,18 @@ const DailyOrderReport: FC = () => {
} }
}; };
if (isPermissionsLoading) {
return <PageLoading />;
}
if (!checkPermission(PermissionEnum.VIEW_ORDER_REPORTS)) {
return (
<div className='mt-5 text-sm text-gray-500'>
شما دسترسی مشاهده این گزارش را ندارید.
</div>
);
}
return ( return (
<div className='mt-5'> <div className='mt-5'>
<div className='flex justify-between items-center'> <div className='flex justify-between items-center'>
+16
View File
@@ -4,6 +4,9 @@ import { DocumentDownload } from 'iconsax-react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import Filters from '@/components/Filters'; import Filters from '@/components/Filters';
import Button from '@/components/Button'; import Button from '@/components/Button';
import PageLoading from '@/components/PageLoading';
import { usePermissions } from '@/hooks/usePermissions';
import { PermissionEnum } from '@/pages/roles/enum/Enum';
import { useGetFoodOrderReport } from './hooks/useFoodOrderReportData'; import { useGetFoodOrderReport } from './hooks/useFoodOrderReportData';
import { useFoodOrderReportFilters } from './hooks/useFoodOrderReportFilters'; import { useFoodOrderReportFilters } from './hooks/useFoodOrderReportFilters';
import { getFoodOrderReportTableColumns } from './components/FoodOrderReportTableColumns'; import { getFoodOrderReportTableColumns } from './components/FoodOrderReportTableColumns';
@@ -15,6 +18,7 @@ import type { RowDataType } from '@/components/types/TableTypes';
const FoodOrderReport: FC = () => { const FoodOrderReport: FC = () => {
const { t } = useTranslation('global'); const { t } = useTranslation('global');
const { checkPermission, isLoading: isPermissionsLoading } = usePermissions();
const { filters, apiParams, handleFiltersChange } = useFoodOrderReportFilters(); const { filters, apiParams, handleFiltersChange } = useFoodOrderReportFilters();
const { data: reportData, isLoading } = useGetFoodOrderReport(apiParams); const { data: reportData, isLoading } = useGetFoodOrderReport(apiParams);
const [isExporting, setIsExporting] = useState(false); const [isExporting, setIsExporting] = useState(false);
@@ -43,6 +47,18 @@ const FoodOrderReport: FC = () => {
} }
}; };
if (isPermissionsLoading) {
return <PageLoading />;
}
if (!checkPermission(PermissionEnum.VIEW_ORDER_REPORTS)) {
return (
<div className='mt-5 text-sm text-gray-500'>
شما دسترسی مشاهده این گزارش را ندارید.
</div>
);
}
return ( return (
<div className='mt-5'> <div className='mt-5'>
<div className='flex justify-between items-center'> <div className='flex justify-between items-center'>
@@ -7,6 +7,7 @@ import { Link } from 'react-router-dom'
import { Pages } from '@/config/Pages' import { Pages } from '@/config/Pages'
import { formatFaNumber } from '@/helpers/func' import { formatFaNumber } from '@/helpers/func'
import type { TFunction } from 'i18next' import type { TFunction } from 'i18next'
import { PaymentStatusEnum } from '../enum/Enum'
interface GetOrderTableColumnsParams { interface GetOrderTableColumnsParams {
onDelete?: (id: string) => void onDelete?: (id: string) => void
@@ -27,6 +28,30 @@ const getStatusVariant = (status: string): 'success' | 'error' | 'warning' | 'in
return variantMap[status] || 'pending' return variantMap[status] || 'pending'
} }
const getPaymentStatusVariant = (status: string): 'success' | 'error' | 'warning' | 'info' | 'pending' => {
const variantMap: Record<string, 'success' | 'error' | 'warning' | 'info' | 'pending'> = {
[PaymentStatusEnum.Paid]: 'success',
[PaymentStatusEnum.Pending]: 'warning',
[PaymentStatusEnum.Failed]: 'error',
[PaymentStatusEnum.Refunded]: 'info',
}
return variantMap[status] || 'pending'
}
const getPaymentStatusLabel = (status: string): string => {
const labelMap: Record<string, string> = {
[PaymentStatusEnum.Paid]: 'پرداخت شده',
[PaymentStatusEnum.Pending]: 'در انتظار پرداخت',
[PaymentStatusEnum.Failed]: 'پرداخت ناموفق',
[PaymentStatusEnum.Refunded]: 'بازگشت شده',
}
return labelMap[status] || '-'
}
const resolvePaymentStatus = (item: Order): string | undefined => {
return item.paymentStatus || item.payments?.[0]?.status
}
export const getOrderTableColumns = ({ t }: GetOrderTableColumnsParams): ColumnType<Order>[] => { export const getOrderTableColumns = ({ t }: GetOrderTableColumnsParams): ColumnType<Order>[] => {
return [ return [
{ {
@@ -78,6 +103,20 @@ export const getOrderTableColumns = ({ t }: GetOrderTableColumnsParams): ColumnT
return item.paymentMethod?.description || '-' return item.paymentMethod?.description || '-'
} }
}, },
{
key: 'paymentStatus',
title: 'وضعیت پرداخت',
render: (item: Order) => {
const paymentStatus = resolvePaymentStatus(item)
if (!paymentStatus) return '-'
return (
<Status
variant={getPaymentStatusVariant(paymentStatus)}
label={getPaymentStatusLabel(paymentStatus)}
/>
)
}
},
{ {
key: 'status', key: 'status',
title: 'وضعیت', title: 'وضعیت',
+3
View File
@@ -6,6 +6,9 @@ export const enum PermissionEnum {
// Order Management // Order Management
MANAGE_ORDERS = "manage_orders", MANAGE_ORDERS = "manage_orders",
VIEW_ORDER_REPORTS = "view_order_reports",
MANAGE_SHIFTS = "manage_shifts",
VIEW_ALL_SHIFTS = "view_all_shifts",
// User & Admin Management // User & Admin Management
MANAGE_ADMINS = "manage_admins", MANAGE_ADMINS = "manage_admins",
+2 -2
View File
@@ -102,12 +102,12 @@ const SideBar: FC = () => {
/> />
)} )}
{checkPermission(PermissionEnum.MANAGE_ORDERS) && ( {(checkPermission(PermissionEnum.MANAGE_ORDERS) || checkPermission(PermissionEnum.VIEW_ORDER_REPORTS)) && (
<SideBarItem <SideBarItem
icon={<DocumentText variant={isActive('orders') ? 'Bold' : 'Outline'} color={isActive('orders') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />} icon={<DocumentText variant={isActive('orders') ? 'Bold' : 'Outline'} color={isActive('orders') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
title={t('sidebar.orders')} title={t('sidebar.orders')}
isActive={isActive('orders')} isActive={isActive('orders')}
link={Pages.orders.list} link={checkPermission(PermissionEnum.MANAGE_ORDERS) ? Pages.orders.list : Pages.orders.foodReport}
name='orders' name='orders'
activeName='orders' activeName='orders'
/> />
+38 -25
View File
@@ -4,11 +4,14 @@ import { useTranslation } from 'react-i18next'
import SubMenuItem from './SubMenuItem' import SubMenuItem from './SubMenuItem'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { Pages } from '../../config/Pages' import { Pages } from '../../config/Pages'
import { usePermissions } from '@/hooks/usePermissions'
import { PermissionEnum } from '@/pages/roles/enum/Enum'
const OrdersSubMenu: FC = () => { const OrdersSubMenu: FC = () => {
const location = useLocation() const location = useLocation()
const { t } = useTranslation('global') const { t } = useTranslation('global')
const { checkPermission } = usePermissions()
const isActive = (name: string) => { const isActive = (name: string) => {
return location.pathname.includes(name) return location.pathname.includes(name)
@@ -28,31 +31,41 @@ const OrdersSubMenu: FC = () => {
</div> </div>
<div className='flex flex-col mt-10 gap-4'> <div className='flex flex-col mt-10 gap-4'>
<SubMenuItem {checkPermission(PermissionEnum.MANAGE_ORDERS) && (
title={t('submenu.orders_list')} <SubMenuItem
isActive={isActive('list')} title={t('submenu.orders_list')}
link={Pages.orders.list} isActive={isActive('list')}
/> link={Pages.orders.list}
<SubMenuItem />
title={t('submenu.orders_food_report')} )}
isActive={isActive('report/by-food')} {checkPermission(PermissionEnum.VIEW_ORDER_REPORTS) && (
link={Pages.orders.foodReport} <SubMenuItem
/> title={t('submenu.orders_food_report')}
<SubMenuItem isActive={isActive('report/by-food')}
title={t('submenu.orders_daily_report')} link={Pages.orders.foodReport}
isActive={isActive('report/by-day')} />
link={Pages.orders.dailyReport} )}
/> {checkPermission(PermissionEnum.VIEW_ORDER_REPORTS) && (
<SubMenuItem <SubMenuItem
title={t('submenu.cash_shifts')} title={t('submenu.orders_daily_report')}
isActive={isActive('cash-shifts')} isActive={isActive('report/by-day')}
link={Pages.orders.cashShifts} link={Pages.orders.dailyReport}
/> />
<SubMenuItem )}
title={t('submenu.add_order')} {checkPermission(PermissionEnum.MANAGE_ORDERS) && (
isActive={isActive('add')} <SubMenuItem
link={Pages.orders.add} title={t('submenu.cash_shifts')}
/> isActive={isActive('cash-shifts')}
link={Pages.orders.cashShifts}
/>
)}
{checkPermission(PermissionEnum.MANAGE_ORDERS) && (
<SubMenuItem
title={t('submenu.add_order')}
isActive={isActive('add')}
link={Pages.orders.add}
/>
)}
</div> </div>
</div> </div>
) )