feat: additional expenses

This commit is contained in:
2024-11-26 01:37:15 +04:00
parent e3146832a5
commit 564895c26f
18 changed files with 430 additions and 12 deletions

View File

@@ -132,6 +132,7 @@ export type { DealUpdateServiceQuantityRequest } from './models/DealUpdateServic
export type { DealUpdateServiceQuantityResponse } from './models/DealUpdateServiceQuantityResponse'; export type { DealUpdateServiceQuantityResponse } from './models/DealUpdateServiceQuantityResponse';
export type { DealUpdateServiceRequest } from './models/DealUpdateServiceRequest'; export type { DealUpdateServiceRequest } from './models/DealUpdateServiceRequest';
export type { DealUpdateServiceResponse } from './models/DealUpdateServiceResponse'; export type { DealUpdateServiceResponse } from './models/DealUpdateServiceResponse';
export type { DeleteExpenseResponse } from './models/DeleteExpenseResponse';
export type { DeleteMarketplaceRequest } from './models/DeleteMarketplaceRequest'; export type { DeleteMarketplaceRequest } from './models/DeleteMarketplaceRequest';
export type { DeleteMarketplaceResponse } from './models/DeleteMarketplaceResponse'; export type { DeleteMarketplaceResponse } from './models/DeleteMarketplaceResponse';
export type { DeletePaymentRecordRequest } from './models/DeletePaymentRecordRequest'; export type { DeletePaymentRecordRequest } from './models/DeletePaymentRecordRequest';
@@ -145,12 +146,14 @@ export type { DeletePriceCategoryResponse } from './models/DeletePriceCategoryRe
export type { DeleteShiftResponse } from './models/DeleteShiftResponse'; export type { DeleteShiftResponse } from './models/DeleteShiftResponse';
export type { DeleteShippingWarehouseRequest } from './models/DeleteShippingWarehouseRequest'; export type { DeleteShippingWarehouseRequest } from './models/DeleteShippingWarehouseRequest';
export type { DeleteShippingWarehouseResponse } from './models/DeleteShippingWarehouseResponse'; export type { DeleteShippingWarehouseResponse } from './models/DeleteShippingWarehouseResponse';
export type { ExpenseSchemaBase } from './models/ExpenseSchemaBase';
export type { FinishShiftByIdResponse } from './models/FinishShiftByIdResponse'; export type { FinishShiftByIdResponse } from './models/FinishShiftByIdResponse';
export type { FinishShiftResponse } from './models/FinishShiftResponse'; export type { FinishShiftResponse } from './models/FinishShiftResponse';
export type { GetAllBarcodeTemplateAttributesResponse } from './models/GetAllBarcodeTemplateAttributesResponse'; export type { GetAllBarcodeTemplateAttributesResponse } from './models/GetAllBarcodeTemplateAttributesResponse';
export type { GetAllBarcodeTemplateSizesResponse } from './models/GetAllBarcodeTemplateSizesResponse'; export type { GetAllBarcodeTemplateSizesResponse } from './models/GetAllBarcodeTemplateSizesResponse';
export type { GetAllBarcodeTemplatesResponse } from './models/GetAllBarcodeTemplatesResponse'; export type { GetAllBarcodeTemplatesResponse } from './models/GetAllBarcodeTemplatesResponse';
export type { GetAllBaseMarketplacesResponse } from './models/GetAllBaseMarketplacesResponse'; export type { GetAllBaseMarketplacesResponse } from './models/GetAllBaseMarketplacesResponse';
export type { GetAllExpensesResponse } from './models/GetAllExpensesResponse';
export type { GetAllPayRatesResponse } from './models/GetAllPayRatesResponse'; export type { GetAllPayRatesResponse } from './models/GetAllPayRatesResponse';
export type { GetAllPayrollSchemeResponse } from './models/GetAllPayrollSchemeResponse'; export type { GetAllPayrollSchemeResponse } from './models/GetAllPayrollSchemeResponse';
export type { GetAllPositionsResponse } from './models/GetAllPositionsResponse'; export type { GetAllPositionsResponse } from './models/GetAllPositionsResponse';
@@ -243,6 +246,9 @@ export type { SynchronizeMarketplaceRequest } from './models/SynchronizeMarketpl
export type { TaskInfoResponse } from './models/TaskInfoResponse'; export type { TaskInfoResponse } from './models/TaskInfoResponse';
export type { TimeTrackingData } from './models/TimeTrackingData'; export type { TimeTrackingData } from './models/TimeTrackingData';
export type { TimeTrackingRecord } from './models/TimeTrackingRecord'; export type { TimeTrackingRecord } from './models/TimeTrackingRecord';
export type { UpdateExpenseRequest } from './models/UpdateExpenseRequest';
export type { UpdateExpenseResponse } from './models/UpdateExpenseResponse';
export type { UpdateExpenseSchema } from './models/UpdateExpenseSchema';
export type { UpdateMarketplaceRequest } from './models/UpdateMarketplaceRequest'; export type { UpdateMarketplaceRequest } from './models/UpdateMarketplaceRequest';
export type { UpdateMarketplaceResponse } from './models/UpdateMarketplaceResponse'; export type { UpdateMarketplaceResponse } from './models/UpdateMarketplaceResponse';
export type { UpdatePayRateRequest } from './models/UpdatePayRateRequest'; export type { UpdatePayRateRequest } from './models/UpdatePayRateRequest';
@@ -268,6 +274,7 @@ export { BarcodeService } from './services/BarcodeService';
export { BillingService } from './services/BillingService'; export { BillingService } from './services/BillingService';
export { ClientService } from './services/ClientService'; export { ClientService } from './services/ClientService';
export { DealService } from './services/DealService'; export { DealService } from './services/DealService';
export { ExpenseService } from './services/ExpenseService';
export { MarketplaceService } from './services/MarketplaceService'; export { MarketplaceService } from './services/MarketplaceService';
export { PayrollService } from './services/PayrollService'; export { PayrollService } from './services/PayrollService';
export { PositionService } from './services/PositionService'; export { PositionService } from './services/PositionService';

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DeleteExpenseResponse = {
ok: boolean;
message: string;
};

View File

@@ -0,0 +1,14 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { UserSchema } from './UserSchema';
export type ExpenseSchemaBase = {
id: number;
name: string;
comment: string;
amount: number;
createdByUser: UserSchema;
spentDate: string;
};

View File

@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ExpenseSchemaBase } from './ExpenseSchemaBase';
import type { PaginationInfoSchema } from './PaginationInfoSchema';
export type GetAllExpensesResponse = {
expenses: Array<ExpenseSchemaBase>;
paginationInfo: PaginationInfoSchema;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { UpdateExpenseSchema } from './UpdateExpenseSchema';
export type UpdateExpenseRequest = {
expense: UpdateExpenseSchema;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UpdateExpenseResponse = {
ok: boolean;
message: string;
};

View File

@@ -0,0 +1,12 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UpdateExpenseSchema = {
id?: (number | null);
name: string;
comment?: (string | null);
amount: number;
spentDate: string;
};

View File

@@ -0,0 +1,78 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { DeleteExpenseResponse } from '../models/DeleteExpenseResponse';
import type { GetAllExpensesResponse } from '../models/GetAllExpensesResponse';
import type { UpdateExpenseRequest } from '../models/UpdateExpenseRequest';
import type { UpdateExpenseResponse } from '../models/UpdateExpenseResponse';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class ExpenseService {
/**
* Get All
* @returns GetAllExpensesResponse Successful Response
* @throws ApiError
*/
public static getAllExpenses({
page,
itemsPerPage,
}: {
page?: (number | null),
itemsPerPage?: (number | null),
}): CancelablePromise<GetAllExpensesResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/expense/get-all',
query: {
'page': page,
'items_per_page': itemsPerPage,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Update Expense
* @returns UpdateExpenseResponse Successful Response
* @throws ApiError
*/
public static updateExpense({
requestBody,
}: {
requestBody: UpdateExpenseRequest,
}): CancelablePromise<UpdateExpenseResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/expense/update',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Delete Expense
* @returns DeleteExpenseResponse Successful Response
* @throws ApiError
*/
public static deleteExpense({
expenseId,
}: {
expenseId: number,
}): CancelablePromise<DeleteExpenseResponse> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/expense/delete/{expense_id}',
path: {
'expense_id': expenseId,
},
errors: {
422: `Validation Error`,
},
});
}
}

View File

@@ -22,6 +22,7 @@ import ShippingWarehouseForm from "../pages/ShippingWarehousesPage/modals/Shippi
import MarketplaceFormModal from "../pages/MarketplacesPage/modals/MarketplaceFormModal/MarketplaceFormModal.tsx"; import MarketplaceFormModal from "../pages/MarketplacesPage/modals/MarketplaceFormModal/MarketplaceFormModal.tsx";
import ServicePriceCategoryForm from "../pages/ServicesPage/modals/ServicePriceCategoryForm.tsx"; import ServicePriceCategoryForm from "../pages/ServicesPage/modals/ServicePriceCategoryForm.tsx";
import ScanningModal from "./ScanningModal/ScanningModal.tsx"; import ScanningModal from "./ScanningModal/ScanningModal.tsx";
import ExpenseFormModal from "../pages/AdminPage/tabs/Expenses/modals/ExpenseFormModal.tsx";
export const modals = { export const modals = {
enterDeadline: EnterDeadlineModal, enterDeadline: EnterDeadlineModal,
@@ -48,4 +49,5 @@ export const modals = {
marketplaceFormModal: MarketplaceFormModal, marketplaceFormModal: MarketplaceFormModal,
servicePriceCategoryForm: ServicePriceCategoryForm, servicePriceCategoryForm: ServicePriceCategoryForm,
scanningModal: ScanningModal, scanningModal: ScanningModal,
expenseFormModal: ExpenseFormModal,
}; };

View File

@@ -3,7 +3,7 @@ import { Tabs } from "@mantine/core";
import PageBlock from "../../components/PageBlock/PageBlock.tsx"; import PageBlock from "../../components/PageBlock/PageBlock.tsx";
import { import {
IconBriefcase, IconBriefcase,
IconCalendarUser, IconCalendarUser, IconCoins,
IconCurrencyDollar, IconQrcode, IconCurrencyDollar, IconQrcode,
IconUser, IconUser,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
@@ -13,6 +13,7 @@ import { motion } from "framer-motion";
import FinancesTab from "./tabs/Finances/FinancesTab.tsx"; import FinancesTab from "./tabs/Finances/FinancesTab.tsx";
import WorkTimeTable from "./tabs/WorkTimeTable/ui/WorkTimeTable.tsx"; import WorkTimeTable from "./tabs/WorkTimeTable/ui/WorkTimeTable.tsx";
import { WorkShiftsTab } from "./tabs/WorkShifts/WorkShiftsTab.tsx"; import { WorkShiftsTab } from "./tabs/WorkShifts/WorkShiftsTab.tsx";
import { ExpensesTab } from "./tabs/Expenses/ExpensesTab.tsx";
const AdminPage = () => { const AdminPage = () => {
return ( return (
@@ -48,6 +49,11 @@ const AdminPage = () => {
leftSection={<IconQrcode />}> leftSection={<IconQrcode />}>
Смены Смены
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab
value={"expenses"}
leftSection={<IconCoins />}>
Расходы
</Tabs.Tab>
</Tabs.List> </Tabs.List>
<Tabs.Panel value={"users"}> <Tabs.Panel value={"users"}>
<motion.div <motion.div
@@ -89,6 +95,14 @@ const AdminPage = () => {
<WorkShiftsTab /> <WorkShiftsTab />
</motion.div> </motion.div>
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value={"expenses"}>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}>
<ExpensesTab />
</motion.div>
</Tabs.Panel>
</Tabs> </Tabs>
</PageBlock> </PageBlock>
</div> </div>

View File

@@ -0,0 +1,12 @@
import { Pagination } from "../../../types/Pagination.ts";
import { ObjectListWithPagination } from "../../../hooks/objectList.tsx";
import { ExpenseService } from "../../../client";
export const useExpensesList = (pagination: Pagination) =>
ObjectListWithPagination({
queryFn: () => ExpenseService.getAllExpenses(pagination),
queryKey: "getExpenses",
getObjectsFn: response => response.expenses,
pagination,
});

View File

@@ -0,0 +1,134 @@
import { useExpensesList } from "../../hooks/useExpensesList.tsx";
import { useExpensesTableColumns } from "./hooks/useExpensesTableColumns.tsx";
import { ExpenseSchemaBase, ExpenseService } from "../../../../client";
import { dateToString } from "../../../../types/utils.ts";
import { notifications } from "../../../../shared/lib/notifications.ts";
import { modals } from "@mantine/modals";
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
import { Pagination, Flex, rem, Button, Tooltip, ActionIcon } from "@mantine/core";
import { useEffect, useState } from "react";
import { IconEdit, IconTrash } from "@tabler/icons-react";
import { MRT_TableOptions } from "mantine-react-table";
export const ExpensesTab = () => {
const [totalPages, setTotalPages] = useState(10);
const [page, setPage] = useState(1);
const {
pagination: paginationInfo,
objects: expenses,
refetch,
} = useExpensesList({ page: page, itemsPerPage: 10 });
const columns = useExpensesTableColumns();
useEffect(() => {
if (!paginationInfo) return;
setTotalPages(paginationInfo.totalPages);
}, [paginationInfo]);
const onUpdate = (expense: ExpenseSchemaBase) => {
ExpenseService.updateExpense({
requestBody: {
expense: {
...expense,
spentDate: dateToString(new Date(expense.spentDate)) ?? "",
},
},
})
.then(async ({ ok, message }) => {
notifications.guess(ok, { message });
await refetch();
})
.catch(err => console.log(err));
};
const onCreateClick = () => {
modals.openContextModal({
modal: "expenseFormModal",
title: "Создание записи о расходах",
withCloseButton: false,
innerProps: {
onCreate: onUpdate,
},
});
};
const onEditClick = (expense: ExpenseSchemaBase) => {
modals.openContextModal({
modal: "expenseFormModal",
title: "Редактирование записи о расходах",
withCloseButton: false,
innerProps: {
onChange: event => onUpdate(event),
element: expense,
},
});
};
const onDeleteClick = (expense: ExpenseSchemaBase) => {
ExpenseService.deleteExpense({
expenseId: expense.id,
})
.then(async ({ ok, message }) => {
notifications.guess(ok, { message });
await refetch();
})
.catch(err => console.log(err));
};
return (
<Flex
direction={"column"}
h={"100%"}
gap={rem(10)}>
<BaseTable
data={expenses}
columns={columns}
restProps={
{
enableTopToolbar: true,
enableRowActions: true,
enableSorting: false,
enableColumnActions: false,
renderTopToolbar: (
<Flex p={rem(10)}>
<Button
variant={"default"}
onClick={() => onCreateClick()}>
Создать запись о расходах
</Button>
</Flex>
),
renderRowActions: ({ row }) => (
<Flex gap="md">
<Tooltip label="Редактировать">
<ActionIcon
onClick={() => onEditClick(row.original)}
variant={"default"}>
<IconEdit />
</ActionIcon>
</Tooltip>
<Tooltip label="Удалить">
<ActionIcon
onClick={() => onDeleteClick(row.original)}
variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
</Flex>
),
} as MRT_TableOptions<ExpenseSchemaBase>
}
/>
{totalPages > 1 && (
<Pagination
style={{ alignSelf: "flex-end" }}
withEdges
onChange={event => setPage(event)}
value={page}
total={totalPages}
/>
)}
</Flex>
);
};

View File

@@ -0,0 +1,37 @@
import { useMemo } from "react";
import { MRT_ColumnDef } from "mantine-react-table";
import { ExpenseSchemaBase } from "../../../../../client";
import { formatDate } from "../../../../../types/utils.ts";
export const useExpensesTableColumns = () => {
return useMemo<MRT_ColumnDef<ExpenseSchemaBase>[]>(
() => [
{
accessorKey: "spentDate",
header: "Дата",
Cell: ({ row }) => formatDate(row.original.spentDate as string),
},
{
accessorKey: "name",
header: "Наименование",
},
{
accessorKey: "comment",
header: "Комментарий",
},
{
accessorKey: "amount",
header: "Сумма",
},
{
accessorKey: "createdByUser",
header: "Создал запись",
Cell: ({ row }) => {
return `${row.original.createdByUser.firstName} ${row.original.createdByUser.secondName}`;
},
},
],
[],
);
};

View File

@@ -0,0 +1,67 @@
import { ContextModalProps } from "@mantine/modals";
import { useForm } from "@mantine/form";
import { Flex, NumberInput, rem, TextInput } from "@mantine/core";
import BaseFormModal, { CreateEditFormProps } from "../../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
import { ExpenseSchemaBase } from "../../../../../client";
import { DatePickerInput } from "@mantine/dates";
type Props = CreateEditFormProps<ExpenseSchemaBase>;
const ExpenseFormModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const isEditing = "element" in innerProps;
const initialValue: Partial<ExpenseSchemaBase> = isEditing
? innerProps.element
: {};
const form = useForm<Partial<ExpenseSchemaBase>>({
initialValues: initialValue,
validate: {
name: name => !name && "Необходимо указать наименование",
amount: amount => !amount && "Необходимо указать сумму",
spentDate: spentDate => !spentDate && "Необходимо указать дату",
},
});
return (
<BaseFormModal
form={form}
closeOnSubmit
onClose={() => context.closeContextModal(id)}
{...innerProps}>
<BaseFormModal.Body>
<Flex
direction={"column"}
gap={rem(10)}>
<TextInput
label={"Наименование"}
placeholder={"Введите наименование"}
{...form.getInputProps("name")}
/>
<TextInput
label={"Комментарий"}
placeholder={"Введите комментарий"}
{...form.getInputProps("comment")}
/>
<NumberInput
label={"Сумма"}
placeholder={"Введите сумму"}
min={0}
{...form.getInputProps("amount")}
/>
<DatePickerInput
{...form.getInputProps("spentDate")}
value={form.getInputProps("spentDate").value ? new Date(form.getInputProps("spentDate").value) : null}
label={"Дата"}
placeholder={"Укажите дату расхода"}
valueFormat={"DD.MM.YYYY"}
/>
</Flex>
</BaseFormModal.Body>
</BaseFormModal>
);
};
export default ExpenseFormModal;

View File

@@ -1,9 +1,10 @@
import { ChartFormFilters } from "../../../../../types/ChartFormFilters.ts"; import { ChartFormFilters } from "../../../../../types/ChartFormFilters.ts";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { dateToString, getDefaultDates } from "../../../../../utils/dates.ts"; import { getDefaultDates } from "../../../../../utils/dates.ts";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ProfitChartDataItem, StatisticsService } from "../../../../../../../client"; import { ProfitChartDataItem, StatisticsService } from "../../../../../../../client";
import { defaultDealStatus } from "../../../../../utils/defaultFilterValues.ts"; import { defaultDealStatus } from "../../../../../utils/defaultFilterValues.ts";
import { dateToString } from "../../../../../../../types/utils.ts";
export const useProfitChart = () => { export const useProfitChart = () => {

View File

@@ -1,11 +1,12 @@
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { TableFormFilters } from "../../../../../types/TableFormFilters.ts"; import { TableFormFilters } from "../../../../../types/TableFormFilters.ts";
import { dateToString, getDefaultDates } from "../../../../../utils/dates.ts"; import { getDefaultDates } from "../../../../../utils/dates.ts";
import { GroupStatisticsTable } from "../../ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx"; import { GroupStatisticsTable } from "../../ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ProfitTableDataItem, StatisticsService } from "../../../../../../../client"; import { ProfitTableDataItem, StatisticsService } from "../../../../../../../client";
import { useProfitMantineTable } from "./useProfitMantineTable.tsx"; import { useProfitMantineTable } from "./useProfitMantineTable.tsx";
import { defaultDealStatus } from "../../../../../utils/defaultFilterValues.ts"; import { defaultDealStatus } from "../../../../../utils/defaultFilterValues.ts";
import { dateToString } from "../../../../../../../types/utils.ts";
export const useProfitTable = () => { export const useProfitTable = () => {

View File

@@ -5,11 +5,3 @@ export const getDefaultDates = (): [Date, Date] => {
return [dateFrom, dateTo]; return [dateFrom, dateTo];
}; };
export const dateToString = (date: Date | null) => {
if (date === null) return null;
const month = date.getMonth() + 1;
const day = date.getDate();
const monthStr = month < 10 ? `0${month}` : month;
const dayStr = day < 10 ? `0${day}` : day;
return `${date.getFullYear()}-${monthStr}-${dayStr}`;
};

View File

@@ -56,4 +56,13 @@ export const strTimeToFloatHours = (time: string): number => {
const minutes = parseInt(values[1]); const minutes = parseInt(values[1]);
return hours + minutes / 60; return hours + minutes / 60;
} }
export const dateToString = (date: Date | null) => {
if (date === null) return null;
const month = date.getMonth() + 1;
const day = date.getDate();
const monthStr = month < 10 ? `0${month}` : month;
const dayStr = day < 10 ? `0${day}` : day;
return `${date.getFullYear()}-${monthStr}-${dayStr}`;
};