This commit is contained in:
@@ -27,6 +27,7 @@ export const Pages = {
|
||||
detail: "/orders/detail/",
|
||||
foodReport: "/orders/report/by-food",
|
||||
dailyReport: "/orders/report/by-day",
|
||||
cashShifts: "/orders/cash-shifts",
|
||||
},
|
||||
customers: {
|
||||
list: "/customers/list",
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
"orders_list": "لیست سفارشات",
|
||||
"orders_food_report": "گزارش فروش به تفکیک غذا",
|
||||
"orders_daily_report": "گزارش فروش روزانه",
|
||||
"cash_shifts": "شیفتهای صندوق",
|
||||
"add_order": "افزودن سفارش",
|
||||
"discount_list": "لیست تخفیف",
|
||||
"add_discount": "افزودن تخفیف",
|
||||
@@ -929,5 +930,42 @@
|
||||
},
|
||||
"change_status": "تغییر وضعیت",
|
||||
"save": "ذخیره"
|
||||
},
|
||||
"cash_shift": {
|
||||
"list_title": "شیفتهای صندوق",
|
||||
"current_shift": "شیفت فعال",
|
||||
"open_shift": "باز کردن شیفت",
|
||||
"close_shift": "بستن شیفت",
|
||||
"open_title": "باز کردن شیفت صندوق",
|
||||
"close_title": "بستن شیفت صندوق",
|
||||
"edit_title": "ویرایش شیفت صندوق",
|
||||
"edit_shift": "ویرایش شیفت",
|
||||
"save_changes": "ذخیره تغییرات",
|
||||
"detail_title": "جزئیات شیفت صندوق",
|
||||
"summary_title": "خلاصه شیفت",
|
||||
"operator": "اپراتور",
|
||||
"status_label": "وضعیت",
|
||||
"opened_at": "شروع شیفت",
|
||||
"closed_at": "پایان شیفت",
|
||||
"opening_amount": "موجودی اولیه",
|
||||
"opening_amount_placeholder": "مبلغ موجودی اولیه صندوق",
|
||||
"opening_amount_required": "موجودی اولیه الزامی است",
|
||||
"expected_amount": "مبلغ مورد انتظار",
|
||||
"counted_amount": "مبلغ شمارششده",
|
||||
"counted_amount_placeholder": "مبلغ شمارششده در صندوق",
|
||||
"counted_amount_required": "مبلغ شمارششده الزامی است",
|
||||
"difference_amount": "اختلاف",
|
||||
"orders_count": "تعداد سفارشات",
|
||||
"notes": "یادداشت",
|
||||
"notes_placeholder": "یادداشت (اختیاری)",
|
||||
"cancel": "انصراف",
|
||||
"view_details": "مشاهده جزئیات",
|
||||
"opened_success": "شیفت صندوق با موفقیت باز شد",
|
||||
"closed_success": "شیفت صندوق با موفقیت بسته شد",
|
||||
"updated_success": "شیفت صندوق با موفقیت ویرایش شد",
|
||||
"status_values": {
|
||||
"open": "باز",
|
||||
"closed": "بسته"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import { type FC, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Add, Edit, Lock1 } from "iconsax-react";
|
||||
import Table from "@/components/Table";
|
||||
import Filters from "@/components/Filters";
|
||||
import Button from "@/components/Button";
|
||||
import type { RowDataType } from "@/components/types/TableTypes";
|
||||
import {
|
||||
useGetCashShifts,
|
||||
useGetCurrentCashShift,
|
||||
useGetCurrentCashShiftSummary,
|
||||
} from "./hooks/useCashShiftData";
|
||||
import { useCashShiftFilters } from "./hooks/useCashShiftFilters";
|
||||
import { getCashShiftTableColumns } from "./components/CashShiftTableColumns";
|
||||
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";
|
||||
import { formatOptionalDate } from "@/helpers/func";
|
||||
import Status from "@/components/Status";
|
||||
import MoonLoader from "react-spinners/MoonLoader";
|
||||
|
||||
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);
|
||||
|
||||
const {
|
||||
filters,
|
||||
currentPage,
|
||||
apiParams,
|
||||
handleFiltersChange,
|
||||
handlePageChange,
|
||||
} = useCashShiftFilters();
|
||||
|
||||
const { data: shiftsData, isLoading } = useGetCashShifts(apiParams);
|
||||
const { data: currentShiftData, isLoading: isCurrentShiftLoading } =
|
||||
useGetCurrentCashShift();
|
||||
|
||||
const currentShift = currentShiftData?.data ?? null;
|
||||
const hasOpenShift = currentShift?.status === CashShiftStatus.OPEN;
|
||||
|
||||
const { data: currentSummaryData, isLoading: isCurrentSummaryLoading } =
|
||||
useGetCurrentCashShiftSummary(hasOpenShift);
|
||||
|
||||
const shifts = shiftsData?.data || [];
|
||||
const totalPages = shiftsData?.meta?.totalPages || 1;
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
getCashShiftTableColumns({
|
||||
t,
|
||||
onView: (shift) => {
|
||||
setSelectedShift(shift);
|
||||
setIsDetailModalVisible(true);
|
||||
},
|
||||
onEdit: (shift) => {
|
||||
setSelectedShift(shift);
|
||||
setIsEditModalVisible(true);
|
||||
},
|
||||
onClose: (shift) => {
|
||||
setSelectedShift(shift);
|
||||
setIsCloseModalVisible(true);
|
||||
},
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
|
||||
const filterFields = useCashShiftFiltersFields();
|
||||
|
||||
const getAdminName = (shift: CashShift) => {
|
||||
const name = [shift.admin?.firstName, shift.admin?.lastName]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
return name || shift.admin?.phone || "-";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-5">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-lg font-light">{t("cash_shift.list_title")}</h1>
|
||||
{!hasOpenShift && (
|
||||
<Button className="w-fit px-6" onClick={() => setIsOpenModalVisible(true)}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Add color="#fff" size={20} />
|
||||
<span>{t("cash_shift.open_shift")}</span>
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isCurrentShiftLoading ? (
|
||||
<div className="mt-6 flex items-center gap-2">
|
||||
<MoonLoader size={16} color="#000" />
|
||||
</div>
|
||||
) : (
|
||||
hasOpenShift &&
|
||||
currentShift && (
|
||||
<div className="mt-6 bg-[#F5F5F5] rounded-2xl p-5 space-y-4">
|
||||
<div className="flex flex-wrap justify-between items-start gap-4">
|
||||
<div>
|
||||
<div className="text-sm font-medium">
|
||||
{t("cash_shift.current_shift")}
|
||||
</div>
|
||||
<div className="text-xs text-description mt-1">
|
||||
{t("cash_shift.operator")}: {getAdminName(currentShift)}
|
||||
</div>
|
||||
<div className="text-xs text-description mt-1">
|
||||
{t("cash_shift.opened_at")}:{" "}
|
||||
{formatOptionalDate(currentShift.openedAt, {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Status
|
||||
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={() => {
|
||||
setSelectedShift(currentShift);
|
||||
setIsCloseModalVisible(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Lock1 color="#fff" size={18} />
|
||||
<span>{t("cash_shift.close_shift")}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CashShiftSummaryCard
|
||||
summary={currentSummaryData?.data}
|
||||
openingAmount={currentShift.openingAmount}
|
||||
isLoading={isCurrentSummaryLoading}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
<div className="mt-8">
|
||||
<Filters
|
||||
fields={filterFields}
|
||||
onChange={handleFiltersChange}
|
||||
initialValues={filters}
|
||||
showClearButton
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Table<CashShift & RowDataType>
|
||||
columns={columns}
|
||||
data={shifts as (CashShift & RowDataType)[]}
|
||||
isloading={isLoading}
|
||||
pagination={{
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange: handlePageChange,
|
||||
}}
|
||||
/>
|
||||
|
||||
<OpenCashShiftModal
|
||||
open={isOpenModalVisible}
|
||||
onClose={() => setIsOpenModalVisible(false)}
|
||||
/>
|
||||
|
||||
<CloseCashShiftModal
|
||||
open={isCloseModalVisible}
|
||||
onClose={() => {
|
||||
setIsCloseModalVisible(false);
|
||||
setSelectedShift(null);
|
||||
}}
|
||||
shift={selectedShift}
|
||||
/>
|
||||
|
||||
<EditCashShiftModal
|
||||
open={isEditModalVisible}
|
||||
onClose={() => {
|
||||
setIsEditModalVisible(false);
|
||||
setSelectedShift(null);
|
||||
}}
|
||||
shift={selectedShift}
|
||||
/>
|
||||
|
||||
<CashShiftDetailModal
|
||||
open={isDetailModalVisible}
|
||||
onClose={() => {
|
||||
setIsDetailModalVisible(false);
|
||||
setSelectedShift(null);
|
||||
}}
|
||||
shift={selectedShift}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CashShiftsList;
|
||||
@@ -0,0 +1,185 @@
|
||||
import { type FC } from "react";
|
||||
import DefaulModal from "@/components/DefaulModal";
|
||||
import Status from "@/components/Status";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
formatFaNumber,
|
||||
formatOptionalDate,
|
||||
formatPrice,
|
||||
} from "@/helpers/func";
|
||||
import { useGetCashShiftSummary } from "../hooks/useCashShiftData";
|
||||
import CashShiftSummaryCard from "./CashShiftSummaryCard";
|
||||
import type { CashShift } from "../types/Types";
|
||||
import { CashShiftStatus } from "../enum/Enum";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
shift: CashShift | null;
|
||||
};
|
||||
|
||||
const getAdminName = (shift: CashShift) => {
|
||||
const name = [shift.admin?.firstName, shift.admin?.lastName]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
return name || shift.admin?.phone || "-";
|
||||
};
|
||||
|
||||
const CashShiftDetailModal: FC<Props> = ({ open, onClose, shift }) => {
|
||||
const { t } = useTranslation("global");
|
||||
|
||||
const { data: summaryData, isLoading: isSummaryLoading } =
|
||||
useGetCashShiftSummary(shift?.id ?? "", open && !!shift?.id);
|
||||
|
||||
if (!shift) return null;
|
||||
|
||||
const summary = summaryData?.data;
|
||||
const isOpen = shift.status === CashShiftStatus.OPEN;
|
||||
const statusVariant = isOpen ? "warning" : "success";
|
||||
|
||||
return (
|
||||
<DefaulModal
|
||||
open={open}
|
||||
close={onClose}
|
||||
isHeader
|
||||
title_header={t("cash_shift.detail_title")}
|
||||
width={720}
|
||||
>
|
||||
<div className="mt-6 space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-gray-500 mb-1 block">
|
||||
{t("cash_shift.operator")}
|
||||
</label>
|
||||
<p className="text-sm font-medium">{getAdminName(shift)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm text-gray-500 mb-1 block">
|
||||
{t("cash_shift.status_label")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<Status
|
||||
variant={statusVariant}
|
||||
label={t(`cash_shift.status_values.${shift.status}`)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm text-gray-500 mb-1 block">
|
||||
{t("cash_shift.opened_at")}
|
||||
</label>
|
||||
<p className="text-sm font-medium">
|
||||
{formatOptionalDate(shift.openedAt, {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm text-gray-500 mb-1 block">
|
||||
{t("cash_shift.closed_at")}
|
||||
</label>
|
||||
<p className="text-sm font-medium">
|
||||
{formatOptionalDate(shift.closedAt, {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm text-gray-500 mb-1 block">
|
||||
{t("cash_shift.opening_amount")}
|
||||
</label>
|
||||
<p className="text-sm font-medium">
|
||||
{formatPrice(shift.openingAmount)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm text-gray-500 mb-1 block">
|
||||
{t("cash_shift.orders_count")}
|
||||
</label>
|
||||
<p className="text-sm font-medium">
|
||||
{formatFaNumber(shift.ordersCount)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!isOpen && shift.expectedAmount !== null && (
|
||||
<div>
|
||||
<label className="text-sm text-gray-500 mb-1 block">
|
||||
{t("cash_shift.expected_amount")}
|
||||
</label>
|
||||
<p className="text-sm font-medium">
|
||||
{formatPrice(shift.expectedAmount)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isOpen && shift.countedAmount !== null && (
|
||||
<div>
|
||||
<label className="text-sm text-gray-500 mb-1 block">
|
||||
{t("cash_shift.counted_amount")}
|
||||
</label>
|
||||
<p className="text-sm font-medium">
|
||||
{formatPrice(shift.countedAmount)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isOpen && shift.differenceAmount !== null && (
|
||||
<div>
|
||||
<label className="text-sm text-gray-500 mb-1 block">
|
||||
{t("cash_shift.difference_amount")}
|
||||
</label>
|
||||
<p
|
||||
className={`text-sm font-medium ${
|
||||
shift.differenceAmount > 0
|
||||
? "text-green-600"
|
||||
: shift.differenceAmount < 0
|
||||
? "text-red-600"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{formatPrice(shift.differenceAmount)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{shift.notes && (
|
||||
<div>
|
||||
<label className="text-sm text-gray-500 mb-1 block">
|
||||
{t("cash_shift.notes")}
|
||||
</label>
|
||||
<p className="text-sm font-medium bg-gray-50 p-3 rounded-lg whitespace-pre-wrap break-words">
|
||||
{shift.notes}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-[#F5F5F5] rounded-2xl p-4">
|
||||
<div className="text-sm font-medium mb-3">
|
||||
{t("cash_shift.summary_title")}
|
||||
</div>
|
||||
<CashShiftSummaryCard
|
||||
summary={summary}
|
||||
openingAmount={shift.openingAmount}
|
||||
isLoading={isSummaryLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DefaulModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CashShiftDetailModal;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useMemo } from "react";
|
||||
import type { FieldType } from "@/components/Filters";
|
||||
import { CashShiftStatus } from "../enum/Enum";
|
||||
|
||||
export const useCashShiftFiltersFields = (): FieldType[] => {
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
type: "select",
|
||||
name: "status",
|
||||
placeholder: "وضعیت شیفت",
|
||||
defaultValue: "",
|
||||
options: [
|
||||
{ label: "همه", value: "" },
|
||||
{ label: "باز", value: CashShiftStatus.OPEN },
|
||||
{ label: "بسته", value: CashShiftStatus.CLOSED },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "datetime",
|
||||
name: "startDate",
|
||||
placeholder: "از تاریخ",
|
||||
},
|
||||
{
|
||||
type: "datetime",
|
||||
name: "endDate",
|
||||
placeholder: "تا تاریخ",
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { type FC } from "react";
|
||||
import MoonLoader from "react-spinners/MoonLoader";
|
||||
import { formatFaNumber, formatPrice } from "@/helpers/func";
|
||||
import type { CashShiftSummary } from "../types/Types";
|
||||
|
||||
type Props = {
|
||||
summary?: CashShiftSummary | null;
|
||||
openingAmount?: number;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
const SummaryItem: FC<{ label: string; value: string }> = ({ label, value }) => (
|
||||
<div className="bg-white rounded-xl px-4 py-3 min-w-[140px]">
|
||||
<div className="text-xs text-description">{label}</div>
|
||||
<div className="text-sm font-medium mt-1">{value}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const CashShiftSummaryCard: FC<Props> = ({ summary, openingAmount = 0, isLoading }) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-6">
|
||||
<MoonLoader size={20} color="#000" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!summary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<SummaryItem label="موجودی اولیه" value={formatPrice(openingAmount)} />
|
||||
<SummaryItem label="نقدی" value={formatPrice(summary.cashReceived)} />
|
||||
<SummaryItem label="آنلاین" value={formatPrice(summary.onlineReceived)} />
|
||||
<SummaryItem label="کارت به کارت" value={formatPrice(summary.creditCardReceived)} />
|
||||
<SummaryItem label="مجموع دریافتی" value={formatPrice(summary.totalReceived)} />
|
||||
<SummaryItem label="مبلغ مورد انتظار" value={formatPrice(summary.expectedAmount)} />
|
||||
<SummaryItem
|
||||
label="تعداد سفارشات"
|
||||
value={formatFaNumber(summary.ordersCount)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CashShiftSummaryCard;
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Edit, Eye, Lock1 } from "iconsax-react";
|
||||
import type { ColumnType } from "@/components/types/TableTypes";
|
||||
import type { CashShift } from "../types/Types";
|
||||
import {
|
||||
formatFaNumber,
|
||||
formatOptionalDate,
|
||||
formatPrice,
|
||||
} from "@/helpers/func";
|
||||
import Status from "@/components/Status";
|
||||
import { CashShiftStatus } from "../enum/Enum";
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
interface GetCashShiftTableColumnsParams {
|
||||
t: TFunction;
|
||||
onView: (shift: CashShift) => void;
|
||||
onEdit?: (shift: CashShift) => void;
|
||||
onClose?: (shift: CashShift) => void;
|
||||
}
|
||||
|
||||
const getAdminName = (shift: CashShift) => {
|
||||
const name = [shift.admin?.firstName, shift.admin?.lastName]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
return name || shift.admin?.phone || "-";
|
||||
};
|
||||
|
||||
export const getCashShiftTableColumns = ({
|
||||
t,
|
||||
onView,
|
||||
onEdit,
|
||||
onClose,
|
||||
}: GetCashShiftTableColumnsParams): ColumnType<CashShift>[] => [
|
||||
{
|
||||
key: "openedAt",
|
||||
title: t("cash_shift.opened_at"),
|
||||
render: (item) =>
|
||||
formatOptionalDate(item.openedAt, {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: "closedAt",
|
||||
title: t("cash_shift.closed_at"),
|
||||
render: (item) =>
|
||||
formatOptionalDate(item.closedAt, {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: "admin",
|
||||
title: t("cash_shift.operator"),
|
||||
render: (item) => getAdminName(item),
|
||||
},
|
||||
{
|
||||
key: "openingAmount",
|
||||
title: t("cash_shift.opening_amount"),
|
||||
render: (item) => formatPrice(item.openingAmount),
|
||||
},
|
||||
{
|
||||
key: "expectedAmount",
|
||||
title: t("cash_shift.expected_amount"),
|
||||
render: (item) =>
|
||||
item.expectedAmount !== null ? formatPrice(item.expectedAmount) : "-",
|
||||
},
|
||||
{
|
||||
key: "differenceAmount",
|
||||
title: t("cash_shift.difference_amount"),
|
||||
render: (item) => {
|
||||
if (item.differenceAmount === null) return "-";
|
||||
const value = formatPrice(item.differenceAmount);
|
||||
if (item.differenceAmount > 0) {
|
||||
return <span className="text-green-600">{value}</span>;
|
||||
}
|
||||
if (item.differenceAmount < 0) {
|
||||
return <span className="text-red-600">{value}</span>;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "ordersCount",
|
||||
title: t("cash_shift.orders_count"),
|
||||
render: (item) => formatFaNumber(item.ordersCount),
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
title: t("cash_shift.status_label"),
|
||||
render: (item) => (
|
||||
<Status
|
||||
variant={item.status === CashShiftStatus.OPEN ? "warning" : "success"}
|
||||
label={t(`cash_shift.status_values.${item.status}`)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
title: "",
|
||||
render: (item) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => onView(item)}
|
||||
className="cursor-pointer hover:opacity-70 transition-opacity"
|
||||
title={t("cash_shift.view_details")}
|
||||
>
|
||||
<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)}
|
||||
className="cursor-pointer hover:opacity-70 transition-opacity"
|
||||
title={t("cash_shift.close_shift")}
|
||||
>
|
||||
<Lock1 size={20} color="#8C90A3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,135 @@
|
||||
import { type FC, 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 {
|
||||
useCloseCashShift,
|
||||
useGetCashShiftSummary,
|
||||
} from "../hooks/useCashShiftData";
|
||||
import CashShiftSummaryCard from "./CashShiftSummaryCard";
|
||||
import type { CashShift } from "../types/Types";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
shift: CashShift | null;
|
||||
};
|
||||
|
||||
const CloseCashShiftModal: FC<Props> = ({ open, onClose, shift }) => {
|
||||
const { t } = useTranslation("global");
|
||||
const [countedAmount, setCountedAmount] = useState("");
|
||||
const [notes, setNotes] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const closeShift = useCloseCashShift();
|
||||
|
||||
const { data: summaryData, isLoading: isSummaryLoading } =
|
||||
useGetCashShiftSummary(shift?.id ?? "", open && !!shift?.id);
|
||||
|
||||
const summary = summaryData?.data;
|
||||
|
||||
const handleClose = () => {
|
||||
setCountedAmount("");
|
||||
setNotes("");
|
||||
setError("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!shift) return;
|
||||
|
||||
const numericAmount = Number(countedAmount);
|
||||
|
||||
if (Number.isNaN(numericAmount) || numericAmount < 0) {
|
||||
setError(t("cash_shift.counted_amount_required"));
|
||||
return;
|
||||
}
|
||||
|
||||
closeShift.mutate(
|
||||
{
|
||||
shiftId: shift.id,
|
||||
payload: {
|
||||
countedAmount: numericAmount,
|
||||
notes: notes.trim() || undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(t("cash_shift.closed_success"));
|
||||
handleClose();
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(extractErrorMessage(err));
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (!shift) return null;
|
||||
|
||||
return (
|
||||
<DefaulModal
|
||||
open={open}
|
||||
close={handleClose}
|
||||
isHeader
|
||||
title_header={t("cash_shift.close_title")}
|
||||
width={720}
|
||||
>
|
||||
<div className="mt-6 space-y-6">
|
||||
<div className="bg-[#F5F5F5] rounded-2xl p-4">
|
||||
<div className="text-sm font-medium mb-3">
|
||||
{t("cash_shift.summary_title")}
|
||||
</div>
|
||||
<CashShiftSummaryCard
|
||||
summary={summary}
|
||||
openingAmount={shift.openingAmount}
|
||||
isLoading={isSummaryLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
name="countedAmount"
|
||||
label={t("cash_shift.counted_amount")}
|
||||
placeholder={t("cash_shift.counted_amount_placeholder")}
|
||||
type="number"
|
||||
seprator
|
||||
value={countedAmount}
|
||||
onChange={(e) => {
|
||||
setCountedAmount(e.target.value);
|
||||
setError("");
|
||||
}}
|
||||
error_text={error}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
name="notes"
|
||||
label={t("cash_shift.notes")}
|
||||
placeholder={t("cash_shift.notes_placeholder")}
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
isNotRequired
|
||||
/>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
label={t("cash_shift.cancel")}
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="bg-gray-100 text-gray-700"
|
||||
/>
|
||||
<Button
|
||||
label={t("cash_shift.close_shift")}
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
isloading={closeShift.isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DefaulModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CloseCashShiftModal;
|
||||
@@ -0,0 +1,125 @@
|
||||
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;
|
||||
@@ -0,0 +1,108 @@
|
||||
import { type FC, 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 { useOpenCashShift } from "../hooks/useCashShiftData";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const OpenCashShiftModal: FC<Props> = ({ open, onClose }) => {
|
||||
const { t } = useTranslation("global");
|
||||
const [openingAmount, setOpeningAmount] = useState("");
|
||||
const [notes, setNotes] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const openShift = useOpenCashShift();
|
||||
|
||||
const handleClose = () => {
|
||||
setOpeningAmount("");
|
||||
setNotes("");
|
||||
setError("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const numericAmount = Number(openingAmount);
|
||||
|
||||
if (Number.isNaN(numericAmount) || numericAmount < 0) {
|
||||
setError(t("cash_shift.opening_amount_required"));
|
||||
return;
|
||||
}
|
||||
|
||||
openShift.mutate(
|
||||
{
|
||||
openingAmount: numericAmount,
|
||||
notes: notes.trim() || undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(t("cash_shift.opened_success"));
|
||||
handleClose();
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(extractErrorMessage(err));
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<DefaulModal
|
||||
open={open}
|
||||
close={handleClose}
|
||||
isHeader
|
||||
title_header={t("cash_shift.open_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.open_shift")}
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
isloading={openShift.isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DefaulModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenCashShiftModal;
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum CashShiftStatus {
|
||||
OPEN = "open",
|
||||
CLOSED = "closed",
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import * as api from "../service/CashShiftService";
|
||||
import type {
|
||||
CloseCashShiftPayload,
|
||||
OpenCashShiftPayload,
|
||||
UpdateCashShiftPayload,
|
||||
} from "../types/Types";
|
||||
import type { GetCashShiftsParams } from "../service/CashShiftService";
|
||||
|
||||
const invalidateCashShiftQueries = (queryClient: ReturnType<typeof useQueryClient>) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["cash-shifts"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["cash-shift-current"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["cash-shift-current-summary"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["cash-shift-summary"] });
|
||||
};
|
||||
|
||||
export const useGetCashShifts = (params?: GetCashShiftsParams) => {
|
||||
return useQuery({
|
||||
queryKey: ["cash-shifts", params],
|
||||
queryFn: () => api.getCashShifts(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCurrentCashShift = () => {
|
||||
return useQuery({
|
||||
queryKey: ["cash-shift-current"],
|
||||
queryFn: () => api.getCurrentCashShift(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCurrentCashShiftSummary = (enabled = true) => {
|
||||
return useQuery({
|
||||
queryKey: ["cash-shift-current-summary"],
|
||||
queryFn: () => api.getCurrentCashShiftSummary(),
|
||||
enabled,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCashShiftSummary = (shiftId: string, enabled = true) => {
|
||||
return useQuery({
|
||||
queryKey: ["cash-shift-summary", shiftId],
|
||||
queryFn: () => api.getCashShiftSummary(shiftId),
|
||||
enabled: enabled && !!shiftId,
|
||||
});
|
||||
};
|
||||
|
||||
export const useOpenCashShift = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (payload: OpenCashShiftPayload) => api.openCashShift(payload),
|
||||
onSuccess: () => invalidateCashShiftQueries(queryClient),
|
||||
});
|
||||
};
|
||||
|
||||
export const useCloseCashShift = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
shiftId,
|
||||
payload,
|
||||
}: {
|
||||
shiftId: string;
|
||||
payload: CloseCashShiftPayload;
|
||||
}) => api.closeCashShift(shiftId, payload),
|
||||
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),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
import { useState, useMemo, useCallback, useEffect } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import type { FilterValues } from "@/components/Filters";
|
||||
|
||||
const DEFAULT_PAGE = 1;
|
||||
const DEFAULT_LIMIT = 10;
|
||||
const DEFAULT_FILTERS: FilterValues = {
|
||||
status: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
};
|
||||
|
||||
const getFiltersFromParams = (searchParams: URLSearchParams): FilterValues => {
|
||||
const filters: FilterValues = { ...DEFAULT_FILTERS };
|
||||
const status = searchParams.get("status");
|
||||
const startDate = searchParams.get("startDate");
|
||||
const endDate = searchParams.get("endDate");
|
||||
|
||||
if (status) filters.status = status;
|
||||
if (startDate) filters.startDate = startDate;
|
||||
if (endDate) filters.endDate = endDate;
|
||||
|
||||
return filters;
|
||||
};
|
||||
|
||||
const getPageFromParams = (searchParams: URLSearchParams): number => {
|
||||
const page = searchParams.get("page");
|
||||
const parsed = page ? parseInt(page, 10) : DEFAULT_PAGE;
|
||||
return Number.isNaN(parsed) || parsed < 1 ? DEFAULT_PAGE : parsed;
|
||||
};
|
||||
|
||||
const buildSearchParams = (filters: FilterValues, page: number): URLSearchParams => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (page > DEFAULT_PAGE) {
|
||||
params.set("page", String(page));
|
||||
}
|
||||
|
||||
if (filters.status) {
|
||||
params.set("status", filters.status as string);
|
||||
}
|
||||
|
||||
if (filters.startDate) {
|
||||
params.set("startDate", filters.startDate as string);
|
||||
}
|
||||
|
||||
if (filters.endDate) {
|
||||
params.set("endDate", filters.endDate as string);
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
export const useCashShiftFilters = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const [filters, setFilters] = useState<FilterValues>(() =>
|
||||
getFiltersFromParams(searchParams)
|
||||
);
|
||||
const [currentPage, setCurrentPage] = useState(() =>
|
||||
getPageFromParams(searchParams)
|
||||
);
|
||||
const [limit] = useState(DEFAULT_LIMIT);
|
||||
|
||||
useEffect(() => {
|
||||
setFilters(getFiltersFromParams(searchParams));
|
||||
setCurrentPage(getPageFromParams(searchParams));
|
||||
}, [searchParams]);
|
||||
|
||||
const updateUrlParams = useCallback(
|
||||
(newFilters: FilterValues, page: number) => {
|
||||
setSearchParams(buildSearchParams(newFilters, page), { replace: true });
|
||||
},
|
||||
[setSearchParams]
|
||||
);
|
||||
|
||||
const apiParams = useMemo(() => {
|
||||
const params: Record<string, string | number> = {
|
||||
page: currentPage,
|
||||
limit,
|
||||
};
|
||||
|
||||
if (filters.status) {
|
||||
params.status = filters.status as string;
|
||||
}
|
||||
|
||||
if (filters.startDate) {
|
||||
params.startDate = filters.startDate as string;
|
||||
}
|
||||
|
||||
if (filters.endDate) {
|
||||
params.endDate = filters.endDate as string;
|
||||
}
|
||||
|
||||
return params;
|
||||
}, [filters, currentPage, limit]);
|
||||
|
||||
const handleFiltersChange = useCallback(
|
||||
(newFilters: FilterValues) => {
|
||||
setFilters(newFilters);
|
||||
setCurrentPage(DEFAULT_PAGE);
|
||||
updateUrlParams(newFilters, DEFAULT_PAGE);
|
||||
},
|
||||
[updateUrlParams]
|
||||
);
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
(page: number) => {
|
||||
setCurrentPage(page);
|
||||
updateUrlParams(filters, page);
|
||||
},
|
||||
[filters, updateUrlParams]
|
||||
);
|
||||
|
||||
return {
|
||||
filters,
|
||||
currentPage,
|
||||
apiParams,
|
||||
handleFiltersChange,
|
||||
handlePageChange,
|
||||
limit,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
import axios from "@/config/axios";
|
||||
import type {
|
||||
CloseCashShiftPayload,
|
||||
GetCashShiftResponse,
|
||||
GetCashShiftsResponse,
|
||||
GetCashShiftSummaryResponse,
|
||||
OpenCashShiftPayload,
|
||||
UpdateCashShiftPayload,
|
||||
} from "../types/Types";
|
||||
|
||||
export interface GetCashShiftsParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
status?: string;
|
||||
adminId?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
orderBy?: string;
|
||||
order?: "asc" | "desc";
|
||||
}
|
||||
|
||||
export const getCashShifts = async (
|
||||
params?: GetCashShiftsParams
|
||||
): Promise<GetCashShiftsResponse> => {
|
||||
const { data } = await axios.get<GetCashShiftsResponse>("/admin/cash-shifts", {
|
||||
params,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getCurrentCashShift = async (): Promise<GetCashShiftResponse> => {
|
||||
const { data } = await axios.get<GetCashShiftResponse>(
|
||||
"/admin/cash-shifts/current"
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getCurrentCashShiftSummary =
|
||||
async (): Promise<GetCashShiftSummaryResponse> => {
|
||||
const { data } = await axios.get<GetCashShiftSummaryResponse>(
|
||||
"/admin/cash-shifts/current/summary"
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getCashShiftSummary = async (
|
||||
shiftId: string
|
||||
): Promise<GetCashShiftSummaryResponse> => {
|
||||
const { data } = await axios.get<GetCashShiftSummaryResponse>(
|
||||
`/admin/cash-shifts/${shiftId}/summary`
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getCashShiftById = async (
|
||||
shiftId: string
|
||||
): Promise<GetCashShiftResponse> => {
|
||||
const { data } = await axios.get<GetCashShiftResponse>(
|
||||
`/admin/cash-shifts/${shiftId}`
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const openCashShift = async (
|
||||
payload: OpenCashShiftPayload
|
||||
): Promise<GetCashShiftResponse> => {
|
||||
const { data } = await axios.post<GetCashShiftResponse>(
|
||||
"/admin/cash-shifts/open",
|
||||
payload
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const closeCashShift = async (
|
||||
shiftId: string,
|
||||
payload: CloseCashShiftPayload
|
||||
): Promise<GetCashShiftResponse> => {
|
||||
const { data } = await axios.patch<GetCashShiftResponse>(
|
||||
`/admin/cash-shifts/${shiftId}/close`,
|
||||
payload
|
||||
);
|
||||
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;
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { IResponse } from "@/types/response.types";
|
||||
import type { RowDataType } from "@/components/types/TableTypes";
|
||||
import type { CashShiftStatus } from "../enum/Enum";
|
||||
|
||||
export interface CashShiftAdmin {
|
||||
id: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
export interface CashShift extends RowDataType {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
openedAt: string;
|
||||
closedAt: string | null;
|
||||
openingAmount: number;
|
||||
expectedAmount: number | null;
|
||||
countedAmount: number | null;
|
||||
differenceAmount: number | null;
|
||||
ordersCount: number;
|
||||
status: CashShiftStatus;
|
||||
notes: string | null;
|
||||
admin: CashShiftAdmin;
|
||||
}
|
||||
|
||||
export interface CashShiftSummary {
|
||||
ordersCount: number;
|
||||
cashReceived: number;
|
||||
onlineReceived: number;
|
||||
creditCardReceived: number;
|
||||
totalReceived: number;
|
||||
expectedAmount: number;
|
||||
}
|
||||
|
||||
export interface PaginationMeta {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export type GetCashShiftsResponse = IResponse<CashShift[]> & {
|
||||
meta: PaginationMeta;
|
||||
};
|
||||
|
||||
export type GetCashShiftResponse = IResponse<CashShift>;
|
||||
export type GetCashShiftSummaryResponse = IResponse<CashShiftSummary | null>;
|
||||
|
||||
export interface OpenCashShiftPayload {
|
||||
openingAmount: number;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface CloseCashShiftPayload {
|
||||
countedAmount: number;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface UpdateCashShiftPayload {
|
||||
openingAmount?: number;
|
||||
notes?: string;
|
||||
}
|
||||
@@ -120,7 +120,9 @@ const OrderDetails: FC = () => {
|
||||
const hasPaymentDescription = Boolean(payment?.description)
|
||||
const hasPaymentAttachments = Boolean(payment?.attachments && payment.attachments.length > 0)
|
||||
const showPaymentReceiptBox = isCreditCardPayment && (hasPaymentDescription || hasPaymentAttachments)
|
||||
const showVerifyCashPaymentButton = isCashPayment && isPaymentPending && !isCanceled
|
||||
const showVerifyPaymentButton = (isCashPayment || isCreditCardPayment) && isPaymentPending && !isCanceled
|
||||
const showPaymentVerificationBox = showPaymentReceiptBox || showVerifyCashPaymentButton
|
||||
|
||||
// نمایش دکمه ارسال به آشپزخانه
|
||||
// برای pendingPayment با پرداخت نقدی یا برای paid
|
||||
@@ -277,7 +279,7 @@ const OrderDetails: FC = () => {
|
||||
getPaymentStatusTextColor={getPaymentStatusTextColor}
|
||||
getPaymentStatusText={getPaymentStatusText}
|
||||
/>
|
||||
{showPaymentReceiptBox && (
|
||||
{showPaymentVerificationBox && (
|
||||
<PaymentVerificationBox
|
||||
order={order}
|
||||
isVerifyingPayment={isVerifyingPayment}
|
||||
|
||||
@@ -17,6 +17,7 @@ const PaymentVerificationBox: FC<PaymentVerificationBoxProps> = ({
|
||||
showVerifyButton,
|
||||
}) => {
|
||||
const payment = order.payments?.[0]
|
||||
const isCash = order.paymentMethod?.method === PaymentMethodEnum.Cash
|
||||
const isCreditCard = order.paymentMethod?.method === PaymentMethodEnum.CreditCard
|
||||
const hasDescription = Boolean(payment?.description)
|
||||
const hasAttachments = Boolean(payment?.attachments && payment.attachments.length > 0)
|
||||
@@ -57,7 +58,7 @@ const PaymentVerificationBox: FC<PaymentVerificationBoxProps> = ({
|
||||
|
||||
{showVerifyButton && (
|
||||
<Button
|
||||
label='تایید پرداخت'
|
||||
label={isCash ? 'تایید پرداخت نقدی' : 'تایید پرداخت'}
|
||||
onClick={onVerifyPayment}
|
||||
isloading={isVerifyingPayment}
|
||||
className='bg-green-600 hover:bg-green-700 w-full'
|
||||
|
||||
@@ -19,6 +19,7 @@ import CreateOrder from '@/pages/orders/Create'
|
||||
import OrderDetails from '@/pages/orders/Details'
|
||||
import FoodOrderReport from '@/pages/orders/FoodOrderReport'
|
||||
import DailyOrderReport from '@/pages/orders/DailyOrderReport'
|
||||
import CashShiftsList from '@/pages/cashShifts/List'
|
||||
import CommentsList from '@/pages/comments/List'
|
||||
import CommentsDetails from '@/pages/comments/Details'
|
||||
import ScheduleList from '@/pages/schedule/List'
|
||||
@@ -93,6 +94,7 @@ const MainRouter: FC = () => {
|
||||
<Route path={Pages.orders.add} element={<CreateOrder />} />
|
||||
<Route path={Pages.orders.foodReport} element={<FoodOrderReport />} />
|
||||
<Route path={Pages.orders.dailyReport} element={<DailyOrderReport />} />
|
||||
<Route path={Pages.orders.cashShifts} element={<CashShiftsList />} />
|
||||
<Route path={Pages.orders.detail + ':id'} element={<OrderDetails />} />
|
||||
<Route path={Pages.comments.list} element={<CommentsList />} />
|
||||
<Route path={Pages.comments.detail + ':id'} element={<CommentsDetails />} />
|
||||
|
||||
@@ -43,6 +43,11 @@ const OrdersSubMenu: FC = () => {
|
||||
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')}
|
||||
|
||||
Reference in New Issue
Block a user