feat: work shifts by QR codes

This commit is contained in:
2024-11-20 13:09:55 +04:00
parent bf774f5c04
commit 39f746f435
15 changed files with 501 additions and 2 deletions

View File

@@ -4,7 +4,7 @@ import PageBlock from "../../components/PageBlock/PageBlock.tsx";
import {
IconBriefcase,
IconCalendarUser,
IconCurrencyDollar,
IconCurrencyDollar, IconQrcode,
IconUser,
} from "@tabler/icons-react";
import RolesAndPositionsTab from "./tabs/RolesAndPositions/RolesAndPositionsTab.tsx";
@@ -12,6 +12,7 @@ import UsersTab from "./tabs/Users/UsersTab.tsx";
import { motion } from "framer-motion";
import FinancesTab from "./tabs/Finances/FinancesTab.tsx";
import WorkTimeTable from "./tabs/WorkTimeTable/ui/WorkTimeTable.tsx";
import { WorkShiftsTab } from "./tabs/WorkShifts/WorkShiftsTab.tsx";
const AdminPage = () => {
return (
@@ -42,6 +43,11 @@ const AdminPage = () => {
leftSection={<IconCalendarUser />}>
Рабочее время
</Tabs.Tab>
<Tabs.Tab
value={"workShifts"}
leftSection={<IconQrcode />}>
Смены
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value={"users"}>
<motion.div
@@ -75,6 +81,14 @@ const AdminPage = () => {
<WorkTimeTable />
</motion.div>
</Tabs.Panel>
<Tabs.Panel value={"workShifts"}>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}>
<WorkShiftsTab />
</motion.div>
</Tabs.Panel>
</Tabs>
</PageBlock>
</div>

View File

@@ -0,0 +1,115 @@
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
import { useActiveShiftsTableColumns } from "./columns.tsx";
import { ActionIcon, Flex, Stack, Text, Title, Tooltip } from "@mantine/core";
import { IconCheck, IconTrash } from "@tabler/icons-react";
import { ActiveWorkShiftSchema, WorkShiftsService } from "../../../../client";
import { modals } from "@mantine/modals";
import { formatDate } from "../../../../types/utils.ts";
import { MRT_TableOptions } from "mantine-react-table";
import { notifications } from "../../../../shared/lib/notifications.ts";
type Props = {
activeShifts: ActiveWorkShiftSchema[];
fetchActiveShifts: () => void;
}
export const ActiveShiftsTable = ({ activeShifts, fetchActiveShifts }: Props) => {
const columns = useActiveShiftsTableColumns();
const onDelete = (workShift: ActiveWorkShiftSchema) => {
WorkShiftsService.deleteWorkShift({
shiftId: workShift.id,
})
.then(({ ok, message }) => {
notifications.guess(ok, { message });
fetchActiveShifts();
})
.catch(err => {
console.log(err);
});
};
const onDeleteClick = (workShift: ActiveWorkShiftSchema) => {
modals.openConfirmModal({
title: "Удаление записи о начале смены",
children: (
<Text size="sm">
Вы уверены что хотите удалить запись о начале смены работника{" "}
{workShift.user.firstName} {workShift.user.secondName} от{" "}
{formatDate(workShift.startedAt)}
</Text>
),
labels: { confirm: "Да", cancel: "Нет" },
confirmProps: { color: "red" },
onConfirm: () => onDelete(workShift),
});
};
const onShiftFinish = (workShift: ActiveWorkShiftSchema) => {
WorkShiftsService.finishWorkShiftById({
shiftId: workShift.id,
})
.then(({ ok, message }) => {
notifications.guess(ok, { message });
fetchActiveShifts();
})
.catch(err => console.log(err));
}
const onShiftFinishClick = (workShift: ActiveWorkShiftSchema) => {
modals.openConfirmModal({
title: "Завершение смены",
children: (
<Text size="sm">
Вы уверены что хотите завершить смену работника{" "}
{workShift.user.firstName} {workShift.user.secondName} от{" "}
{formatDate(workShift.startedAt)}
</Text>
),
labels: { confirm: "Да", cancel: "Нет" },
confirmProps: { color: "red" },
onConfirm: () => onShiftFinish(workShift),
});
}
return (
<Stack mx={"xs"}>
<Title order={4} mt={"md"}>
Активные смены
</Title>
<BaseTable
data={activeShifts}
columns={columns}
restProps={
{
enablePagination: true,
enableRowActions: true,
renderRowActions: ({ row }) => (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon
onClick={() =>
onDeleteClick(row.original)
}
variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
<Tooltip label="Завершить смену">
<ActionIcon
onClick={() =>
onShiftFinishClick(row.original)
}
variant={"default"}>
<IconCheck />
</ActionIcon>
</Tooltip>
</Flex>
),
} as MRT_TableOptions<ActiveWorkShiftSchema>
}
/>
</Stack>
);
};

View File

@@ -0,0 +1,29 @@
import { useMemo } from "react";
import { MRT_ColumnDef } from "mantine-react-table";
import { ActiveWorkShiftSchema } from "../../../../client";
export const useActiveShiftsTableColumns = () => {
return useMemo<MRT_ColumnDef<ActiveWorkShiftSchema>[]>(
() => [
{
header: "Начало смены",
Cell: ({ row }) =>
new Date(row.original.startedAt).toLocaleString("ru-RU"),
},
{
header: "ФИО",
Cell: ({ row }) =>
`${row.original.user.firstName} ${row.original.user.secondName}`,
},
{
header: "Роль",
accessorKey: "user.role.name",
},
{
header: "Должность",
accessorKey: "user.position.name",
}
],
[]
);
};

View File

@@ -4,7 +4,7 @@ import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
import { FC } from "react";
import { ActionIcon, Button, Flex, rem, Text, Tooltip } from "@mantine/core";
import { useUsersTableColumns } from "./columns.tsx";
import { IconEdit, IconTrash } from "@tabler/icons-react";
import { IconEdit, IconQrcode, IconTrash } from "@tabler/icons-react";
import { modals } from "@mantine/modals";
import { MRT_TableOptions } from "mantine-react-table";
@@ -53,6 +53,13 @@ const UsersTable: FC<Props> = ({ items, onChange, onDelete, onCreate }) => {
size: "md",
});
};
const onGenerateQrClick = (user: UserSchema) => {
const pdfWindow = window.open(
`${import.meta.env.VITE_API_URL}/work-shifts/generate-qr-code/${user.id}`,
);
if (!pdfWindow) return;
pdfWindow.print();
};
return (
<BaseTable
data={items}
@@ -92,6 +99,15 @@ const UsersTable: FC<Props> = ({ items, onChange, onDelete, onCreate }) => {
<IconTrash />
</ActionIcon>
</Tooltip>
<Tooltip
onClick={() => {
onGenerateQrClick(row.original);
}}
label="QR-код">
<ActionIcon variant={"default"}>
<IconQrcode />
</ActionIcon>
</Tooltip>
</Flex>
),
} as MRT_TableOptions<UserSchema>

View File

@@ -0,0 +1,92 @@
import { Button, Group, Stack } from "@mantine/core";
import { ActiveWorkShiftSchema, WorkShiftsService } from "../../../../client";
import { useEffect, useState } from "react";
import { notifications } from "../../../../shared/lib/notifications.ts";
import { modals } from "@mantine/modals";
import { ActiveShiftsTable } from "../../components/ActiveShiftsTable/ActiveShiftsTable.tsx";
export const WorkShiftsTab = () => {
let inputType: "StartShift" | "FinishShift" = "StartShift";
const [activeShifts, setActiveShifts] = useState<ActiveWorkShiftSchema[]>([]);
const fetchActiveShifts = () => {
WorkShiftsService.getActiveShifts()
.then(res => {
setActiveShifts(res.shifts);
})
.catch(err => console.log(err));
}
useEffect(() => {
fetchActiveShifts();
}, []);
const onInputFinish = (userIdInput: string) => {
const userId = parseInt(userIdInput);
if (isNaN(userId)) {
notifications.error({ message: "Ошибка, некорректные данные в QR-коде" });
return;
}
if (inputType === "StartShift") {
WorkShiftsService.startShift({
userId: userId!,
})
.then(async ({ ok, message }) => {
notifications.guess(ok, { message });
fetchActiveShifts();
})
.catch(err => console.log(err));
return;
}
WorkShiftsService.finishShift({
userId: userId!,
})
.then(async ({ ok, message }) => {
notifications.guess(ok, { message });
fetchActiveShifts();
})
.catch(err => console.log(err));
};
const onScanningStart = () => {
modals.openContextModal({
modal: "scanningModal",
innerProps: {
label: "Отсканируйте QR-код",
onScan: onInputFinish,
closeOnScan: true,
},
withCloseButton: false,
});
};
const onShiftStart = () => {
inputType = "StartShift";
onScanningStart();
};
const onShiftFinish = () => {
inputType = "FinishShift";
onScanningStart();
};
return (
<Stack>
<Group ml={"xs"} mt={"xs"}>
<Button variant={"default"} onClick={onShiftStart}>
Начать смену
</Button>
<Button variant={"default"} onClick={onShiftFinish}>
Закончить смену
</Button>
</Group>
<ActiveShiftsTable
activeShifts={activeShifts}
fetchActiveShifts={fetchActiveShifts}
/>
</Stack>
);
};