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": "بستن شیفت",
"open_title": "باز کردن شیفت صندوق",
"close_title": "بستن شیفت صندوق",
"edit_title": "ویرایش شیفت صندوق",
"edit_shift": "ویرایش شیفت",
"save_changes": "ذخیره تغییرات",
"detail_title": "جزئیات شیفت صندوق",
"summary_title": "خلاصه شیفت",
"operator": "اپراتور",
@@ -962,7 +959,6 @@
"view_details": "مشاهده جزئیات",
"opened_success": "شیفت صندوق با موفقیت باز شد",
"closed_success": "شیفت صندوق با موفقیت بسته شد",
"updated_success": "شیفت صندوق با موفقیت ویرایش شد",
"status_values": {
"open": "باز",
"closed": "بسته"
+1 -28
View File
@@ -1,6 +1,6 @@
import { type FC, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Add, Edit, Lock1 } from "iconsax-react";
import { Add, Lock1 } from "iconsax-react";
import Table from "@/components/Table";
import Filters from "@/components/Filters";
import Button from "@/components/Button";
@@ -16,7 +16,6 @@ import { useCashShiftFiltersFields } from "./components/CashShiftFiltersFields";
import CashShiftSummaryCard from "./components/CashShiftSummaryCard";
import OpenCashShiftModal from "./components/OpenCashShiftModal";
import CloseCashShiftModal from "./components/CloseCashShiftModal";
import EditCashShiftModal from "./components/EditCashShiftModal";
import CashShiftDetailModal from "./components/CashShiftDetailModal";
import type { CashShift } from "./types/Types";
import { CashShiftStatus } from "./enum/Enum";
@@ -28,7 +27,6 @@ const CashShiftsList: FC = () => {
const { t } = useTranslation("global");
const [isOpenModalVisible, setIsOpenModalVisible] = useState(false);
const [isCloseModalVisible, setIsCloseModalVisible] = useState(false);
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [selectedShift, setSelectedShift] = useState<CashShift | null>(null);
@@ -61,10 +59,6 @@ const CashShiftsList: FC = () => {
setSelectedShift(shift);
setIsDetailModalVisible(true);
},
onEdit: (shift) => {
setSelectedShift(shift);
setIsEditModalVisible(true);
},
onClose: (shift) => {
setSelectedShift(shift);
setIsCloseModalVisible(true);
@@ -129,18 +123,6 @@ const CashShiftsList: FC = () => {
variant="warning"
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
className="w-fit px-5"
onClick={() => {
@@ -199,15 +181,6 @@ const CashShiftsList: FC = () => {
shift={selectedShift}
/>
<EditCashShiftModal
open={isEditModalVisible}
onClose={() => {
setIsEditModalVisible(false);
setSelectedShift(null);
}}
shift={selectedShift}
/>
<CashShiftDetailModal
open={isDetailModalVisible}
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 { CashShift } from "../types/Types";
import {
@@ -13,7 +13,6 @@ import type { TFunction } from "i18next";
interface GetCashShiftTableColumnsParams {
t: TFunction;
onView: (shift: CashShift) => void;
onEdit?: (shift: CashShift) => void;
onClose?: (shift: CashShift) => void;
}
@@ -27,7 +26,6 @@ const getAdminName = (shift: CashShift) => {
export const getCashShiftTableColumns = ({
t,
onView,
onEdit,
onClose,
}: GetCashShiftTableColumnsParams): ColumnType<CashShift>[] => [
{
@@ -112,15 +110,6 @@ export const getCashShiftTableColumns = ({
>
<Eye size={20} color="#8C90A3" />
</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 && (
<button
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 {
CloseCashShiftPayload,
OpenCashShiftPayload,
UpdateCashShiftPayload,
} from "../types/Types";
import type { GetCashShiftsParams } from "../service/CashShiftService";
@@ -65,17 +64,3 @@ export const useCloseCashShift = () => {
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,
GetCashShiftSummaryResponse,
OpenCashShiftPayload,
UpdateCashShiftPayload,
} from "../types/Types";
export interface GetCashShiftsParams {
@@ -81,14 +80,3 @@ export const closeCashShift = async (
);
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;
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 Filters from '@/components/Filters';
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 { useDailyOrderReportFilters } from './hooks/useDailyOrderReportFilters';
import { getDailyOrderReportTableColumns } from './components/DailyOrderReportTableColumns';
@@ -15,6 +18,7 @@ import type { RowDataType } from '@/components/types/TableTypes';
const DailyOrderReport: FC = () => {
const { t } = useTranslation('global');
const { checkPermission, isLoading: isPermissionsLoading } = usePermissions();
const { filters, apiParams, handleFiltersChange } = useDailyOrderReportFilters();
const { data: reportData, isLoading } = useGetDailyOrderReport(apiParams);
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 (
<div className='mt-5'>
<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 Filters from '@/components/Filters';
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 { useFoodOrderReportFilters } from './hooks/useFoodOrderReportFilters';
import { getFoodOrderReportTableColumns } from './components/FoodOrderReportTableColumns';
@@ -15,6 +18,7 @@ import type { RowDataType } from '@/components/types/TableTypes';
const FoodOrderReport: FC = () => {
const { t } = useTranslation('global');
const { checkPermission, isLoading: isPermissionsLoading } = usePermissions();
const { filters, apiParams, handleFiltersChange } = useFoodOrderReportFilters();
const { data: reportData, isLoading } = useGetFoodOrderReport(apiParams);
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 (
<div className='mt-5'>
<div className='flex justify-between items-center'>
@@ -7,6 +7,7 @@ import { Link } from 'react-router-dom'
import { Pages } from '@/config/Pages'
import { formatFaNumber } from '@/helpers/func'
import type { TFunction } from 'i18next'
import { PaymentStatusEnum } from '../enum/Enum'
interface GetOrderTableColumnsParams {
onDelete?: (id: string) => void
@@ -27,6 +28,30 @@ const getStatusVariant = (status: string): 'success' | 'error' | 'warning' | 'in
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>[] => {
return [
{
@@ -78,6 +103,20 @@ export const getOrderTableColumns = ({ t }: GetOrderTableColumnsParams): ColumnT
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',
title: 'وضعیت',
+3
View File
@@ -6,6 +6,9 @@ export const enum PermissionEnum {
// Order Management
MANAGE_ORDERS = "manage_orders",
VIEW_ORDER_REPORTS = "view_order_reports",
MANAGE_SHIFTS = "manage_shifts",
VIEW_ALL_SHIFTS = "view_all_shifts",
// User & Admin Management
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
icon={<DocumentText variant={isActive('orders') ? 'Bold' : 'Outline'} color={isActive('orders') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
title={t('sidebar.orders')}
isActive={isActive('orders')}
link={Pages.orders.list}
link={checkPermission(PermissionEnum.MANAGE_ORDERS) ? Pages.orders.list : Pages.orders.foodReport}
name='orders'
activeName='orders'
/>
+38 -25
View File
@@ -4,11 +4,14 @@ import { useTranslation } from 'react-i18next'
import SubMenuItem from './SubMenuItem'
import { useLocation } from 'react-router-dom'
import { Pages } from '../../config/Pages'
import { usePermissions } from '@/hooks/usePermissions'
import { PermissionEnum } from '@/pages/roles/enum/Enum'
const OrdersSubMenu: FC = () => {
const location = useLocation()
const { t } = useTranslation('global')
const { checkPermission } = usePermissions()
const isActive = (name: string) => {
return location.pathname.includes(name)
@@ -28,31 +31,41 @@ const OrdersSubMenu: FC = () => {
</div>
<div className='flex flex-col mt-10 gap-4'>
<SubMenuItem
title={t('submenu.orders_list')}
isActive={isActive('list')}
link={Pages.orders.list}
/>
<SubMenuItem
title={t('submenu.orders_food_report')}
isActive={isActive('report/by-food')}
link={Pages.orders.foodReport}
/>
<SubMenuItem
title={t('submenu.orders_daily_report')}
isActive={isActive('report/by-day')}
link={Pages.orders.dailyReport}
/>
<SubMenuItem
title={t('submenu.cash_shifts')}
isActive={isActive('cash-shifts')}
link={Pages.orders.cashShifts}
/>
<SubMenuItem
title={t('submenu.add_order')}
isActive={isActive('add')}
link={Pages.orders.add}
/>
{checkPermission(PermissionEnum.MANAGE_ORDERS) && (
<SubMenuItem
title={t('submenu.orders_list')}
isActive={isActive('list')}
link={Pages.orders.list}
/>
)}
{checkPermission(PermissionEnum.VIEW_ORDER_REPORTS) && (
<SubMenuItem
title={t('submenu.orders_food_report')}
isActive={isActive('report/by-food')}
link={Pages.orders.foodReport}
/>
)}
{checkPermission(PermissionEnum.VIEW_ORDER_REPORTS) && (
<SubMenuItem
title={t('submenu.orders_daily_report')}
isActive={isActive('report/by-day')}
link={Pages.orders.dailyReport}
/>
)}
{checkPermission(PermissionEnum.MANAGE_ORDERS) && (
<SubMenuItem
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>
)