move date functions into utils/date; tabbed DriverForm
This commit is contained in:
@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"
|
||||
import { InputBase, ActionIcon, Popover } from "@mantine/core"
|
||||
import { IconCalendar } from "@tabler/icons-react"
|
||||
import { DatePicker } from "@mantine/dates"
|
||||
import dayjs from "dayjs"
|
||||
import { formatDDMMYYYY, parseDDMMYYYY } from "../utils/date"
|
||||
|
||||
interface MaskedDateInputProps {
|
||||
value?: Date | null
|
||||
@ -14,29 +14,6 @@ interface MaskedDateInputProps {
|
||||
maxDate?: Date | undefined
|
||||
}
|
||||
|
||||
function parseDDMMYYYY(input: string): Date | null {
|
||||
const [dd, mm, yyyy] = input.split(".")
|
||||
const day = parseInt(dd, 10)
|
||||
const month = parseInt(mm, 10)
|
||||
const year = parseInt(yyyy, 10)
|
||||
|
||||
if (
|
||||
isNaN(day) || isNaN(month) || isNaN(year) ||
|
||||
day < 1 || day > 31 ||
|
||||
month < 1 || month > 12 ||
|
||||
year < 1900
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const date = dayjs(`${year}-${month}-${day}`, "YYYY-M-D", true)
|
||||
return date.isValid() ? date.toDate() : null
|
||||
}
|
||||
|
||||
function formatDDMMYYYY(date: Date): string {
|
||||
return dayjs(date).format("DD.MM.YYYY")
|
||||
}
|
||||
|
||||
export default function MaskedDateInput({
|
||||
value,
|
||||
onChange,
|
||||
@ -60,49 +37,49 @@ export default function MaskedDateInput({
|
||||
}, [value])
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const raw = e.target.value;
|
||||
const digits = raw.replace(/\D/g, '').slice(0, 8);
|
||||
const raw = e.target.value
|
||||
const digits = raw.replace(/\D/g, '').slice(0, 8)
|
||||
|
||||
let day = digits.slice(0, 2);
|
||||
let month = digits.slice(2, 4);
|
||||
let year = digits.slice(4, 8);
|
||||
let day = digits.slice(0, 2)
|
||||
let month = digits.slice(2, 4)
|
||||
let year = digits.slice(4, 8)
|
||||
|
||||
// Validate day and month only if they are fully entered (2 digits)
|
||||
if (day.length === 2) {
|
||||
const dayNum = parseInt(day, 10);
|
||||
if (dayNum < 1 || dayNum > 31) return; // Invalid day → block update
|
||||
const dayNum = parseInt(day, 10)
|
||||
if (dayNum < 1 || dayNum > 31) return
|
||||
}
|
||||
|
||||
if (month.length === 2) {
|
||||
const monthNum = parseInt(month, 10);
|
||||
if (monthNum < 1 || monthNum > 12) return; // Invalid month → block update
|
||||
const monthNum = parseInt(month, 10)
|
||||
if (monthNum < 1 || monthNum > 12) return
|
||||
}
|
||||
|
||||
if (year.length === 4) {
|
||||
const yearNum = parseInt(year, 10);
|
||||
if (yearNum < 1900) return; // Invalid year → block update
|
||||
if (yearNum < 1900) return
|
||||
}
|
||||
|
||||
// Format the input as DD.MM.YYYY
|
||||
let formatted = day;
|
||||
if (month.length) formatted += '.' + month;
|
||||
if (year.length) formatted += '.' + year;
|
||||
let formatted = day
|
||||
if (month.length) formatted += '.' + month
|
||||
if (year.length) formatted += '.' + year
|
||||
|
||||
// Smart dot-padding (like "1." → "01.")
|
||||
if (/^\d\.$/.test(raw)) {
|
||||
formatted = '0' + raw;
|
||||
formatted = '0' + raw
|
||||
}
|
||||
|
||||
if (/^\d{2}\.\d\.$/.test(raw)) {
|
||||
formatted = raw.replace(/^(\d{2})\.(\d)\.$/, (_, d, m) => `${d}.0${m}.`);
|
||||
formatted = raw.replace(/^(\d{2})\.(\d)\.$/, (_, d, m) => `${d}.0${m}.`)
|
||||
}
|
||||
|
||||
setInputValue(formatted);
|
||||
setInputValue(formatted)
|
||||
|
||||
// Final parsing only if full date is entered
|
||||
if (day.length === 2 && month.length === 2 && year.length === 4) {
|
||||
const parsed = parseDDMMYYYY(formatted);
|
||||
if (parsed) onChange?.(parsed);
|
||||
const parsed = parseDDMMYYYY(formatted)
|
||||
if (parsed) onChange?.(parsed)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -1,12 +1,69 @@
|
||||
import 'dayjs/locale/ru'
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button, TextInput, Table, ActionIcon, Modal, Checkbox } from "@mantine/core"
|
||||
import { Button, TextInput, Table, ActionIcon, Modal, Checkbox, Tabs, Flex } from "@mantine/core"
|
||||
import { DateValue } from "@mantine/dates"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { IconEdit, IconTrash } from "@tabler/icons-react"
|
||||
import { IconEdit, IconLinkPlus, IconPlus, IconTrash } from "@tabler/icons-react"
|
||||
import { fetchDictionary, createDictionaryItem, updateDictionaryItem, deleteDictionaryItem } from "../api/api"
|
||||
import MaskedDateInput from '../components/MaskedDateInput'
|
||||
import { IDriver, IDriverLicense } from '../interfaces/Driver'
|
||||
import { dateToYYYYMMDD } from '../utils/date'
|
||||
|
||||
const DriverLicense = ({ license, handleDeleteLicense }: { license: IDriverLicense, handleDeleteLicense: any }) => {
|
||||
return (
|
||||
<Table.Tr>
|
||||
<Table.Td>{license.series_number}</Table.Td>
|
||||
<Table.Td>{license.form_date}</Table.Td>
|
||||
<Table.Td>{license.to_date}</Table.Td>
|
||||
<Table.Td>{license.categories?.map((c) => c.name_short).join(", ")}</Table.Td>
|
||||
<Table.Td>
|
||||
<ActionIcon color="red" onClick={() => handleDeleteLicense(license.id)}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)
|
||||
}
|
||||
|
||||
const Driver = ({ driver, handleEditDriver, handleDeleteDriver }: { driver: any, handleEditDriver: any, handleDeleteDriver: any }) => {
|
||||
const [opened, setOpened] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table.Tr onClick={() => setOpened(!opened)}>
|
||||
<Table.Td>{driver.fullname}</Table.Td>
|
||||
<Table.Td>{driver.snils}</Table.Td>
|
||||
<Table.Td>{driver.birthday}</Table.Td>
|
||||
<Table.Td>{driver.iin}</Table.Td>
|
||||
<Table.Td>
|
||||
<ActionIcon color="blue" onClick={() => handleEditDriver(driver)} style={{ marginRight: "10px" }}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
<ActionIcon color="red" onClick={() => handleDeleteDriver(driver.id)}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
{opened &&
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={5}>
|
||||
<Flex justify='space-evenly'>
|
||||
<Button leftSection={<IconPlus />}>Добавить водительские права</Button>
|
||||
<Button leftSection={<IconLinkPlus />}>Привязать водительские права</Button>
|
||||
</Flex>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
}
|
||||
|
||||
{driver.license && Array.isArray(driver.license) && driver.license.map((dr: IDriver) => (
|
||||
<Table.Tr>
|
||||
<Table.Td>{JSON.stringify(dr)}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const DriverForm = () => {
|
||||
const [drivers, setDrivers] = useState<any[]>([])
|
||||
@ -49,18 +106,6 @@ const DriverForm = () => {
|
||||
},
|
||||
});
|
||||
|
||||
function dateToYYYYMMDD(date: DateValue) {
|
||||
if (!date) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0'); // Month is 0-indexed
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
const handleAddDriver = async (values: any) => {
|
||||
const date = new Date(Date.parse(values.birthday))
|
||||
|
||||
@ -84,15 +129,15 @@ const DriverForm = () => {
|
||||
};
|
||||
|
||||
const handleDeleteDriver = async (id: number) => {
|
||||
await deleteDictionaryItem("driver", id);
|
||||
setDrivers((prev) => prev.filter((driver) => driver.id !== id));
|
||||
await deleteDictionaryItem("driver", id)
|
||||
setDrivers((prev) => prev.filter((driver) => driver.id !== id))
|
||||
};
|
||||
|
||||
const handleEditDriver = (driver: any) => {
|
||||
driverForm.setValues(driver);
|
||||
setSelectedDriver(driver);
|
||||
setModalOpened(true);
|
||||
};
|
||||
driverForm.setValues(driver)
|
||||
setSelectedDriver(driver)
|
||||
setModalOpened(true)
|
||||
}
|
||||
|
||||
const handleAddLicense = async (values: any) => {
|
||||
if (selectedLicense) {
|
||||
@ -128,115 +173,73 @@ const DriverForm = () => {
|
||||
setSelectedLicense(null);
|
||||
};
|
||||
|
||||
const handleDeleteLicense = async (driverId: number, licenseId: number) => {
|
||||
const handleDeleteLicense = async (licenseId: number) => {
|
||||
await deleteDictionaryItem("driver_license", licenseId);
|
||||
setDrivers((prev) =>
|
||||
prev.map((driver) =>
|
||||
driver.id === driverId ? { ...driver, license: driver.license.filter((lic: IDriverLicense) => lic.id !== licenseId) } : driver
|
||||
({ ...driver, license: driver.license.filter((lic: IDriverLicense) => lic.id !== licenseId) })
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
function parseDDMMYYYY(dateString: string): Date | null {
|
||||
const [day, month, year] = dateString.split('.').map(Number);
|
||||
|
||||
if (
|
||||
isNaN(day) ||
|
||||
isNaN(month) ||
|
||||
isNaN(year) ||
|
||||
month < 1 ||
|
||||
month > 12 ||
|
||||
year < 1000 || // Prevent extremely early dates
|
||||
year > 9999
|
||||
) {
|
||||
return null; // Invalid input
|
||||
}
|
||||
|
||||
return new Date(year, month - 1, day); // Month is 0-indexed in Date
|
||||
}
|
||||
|
||||
const Driver = ({ driver }: { driver: any }) => {
|
||||
const [opened, setOpened] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table.Tr onClick={() => setOpened(!opened)}>
|
||||
<Table.Td>{driver.fullname}</Table.Td>
|
||||
<Table.Td>{driver.snils}</Table.Td>
|
||||
<Table.Td>{driver.birthday}</Table.Td>
|
||||
<Table.Td>{driver.iin}</Table.Td>
|
||||
<Table.Td>
|
||||
<ActionIcon color="blue" onClick={() => handleEditDriver(driver)} style={{ marginRight: "10px" }}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
<ActionIcon color="red" onClick={() => handleDeleteDriver(driver.id)}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
{driver.license && Array.isArray(driver.license) && driver.license.map((dr: IDriver) => (
|
||||
<Table.Tr>
|
||||
<Table.Td>{JSON.stringify(dr)}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Водители</h2>
|
||||
<Button onClick={() => setModalOpened(true)}>Добавить водителя</Button>
|
||||
<Table striped highlightOnHover withColumnBorders mt="md">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>ФИО</Table.Th>
|
||||
<Table.Th>СНИЛС</Table.Th>
|
||||
<Table.Th>Дата рождения</Table.Th>
|
||||
<Table.Th>ИНН</Table.Th>
|
||||
<Table.Th>Действия</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{drivers.map((driver) => (
|
||||
<Driver key={driver.id} driver={driver} />
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
<Tabs defaultValue='drivers'>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value='drivers'>
|
||||
Водители
|
||||
</Tabs.Tab>
|
||||
|
||||
<h2>Водительские права</h2>
|
||||
<Button onClick={() => setLicenseModalOpened(true)}>Добавить права</Button>
|
||||
<Table striped highlightOnHover withColumnBorders mt="md">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>ФИО</Table.Th>
|
||||
<Table.Th>Серия и номер</Table.Th>
|
||||
<Table.Th>Дата выдачи</Table.Th>
|
||||
<Table.Th>Срок действия</Table.Th>
|
||||
<Table.Th>Категории</Table.Th>
|
||||
<Table.Th>Действия</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{drivers.flatMap((driver) =>
|
||||
driver.license?.map((license: IDriverLicense) => (
|
||||
<Table.Tr key={license.id}>
|
||||
<Table.Td>{driver.fullname}</Table.Td>
|
||||
<Table.Td>{license.series_number}</Table.Td>
|
||||
<Table.Td>{license.form_date}</Table.Td>
|
||||
<Table.Td>{license.to_date}</Table.Td>
|
||||
<Table.Td>{license.categories?.map((c) => c.name_short).join(", ")}</Table.Td>
|
||||
<Table.Td>
|
||||
<ActionIcon color="red" onClick={() => handleDeleteLicense(driver.id, license.id)}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
<Tabs.Tab value='licenses'>
|
||||
Водительские права
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value='drivers'>
|
||||
<h2>Водители</h2>
|
||||
<Button onClick={() => setModalOpened(true)}>Добавить водителя</Button>
|
||||
<Table striped highlightOnHover withColumnBorders mt="md">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>ФИО</Table.Th>
|
||||
<Table.Th>СНИЛС</Table.Th>
|
||||
<Table.Th>Дата рождения</Table.Th>
|
||||
<Table.Th>ИНН</Table.Th>
|
||||
<Table.Th>Действия</Table.Th>
|
||||
</Table.Tr>
|
||||
))
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{drivers.map((driver) => (
|
||||
<Driver key={driver.id} driver={driver} handleEditDriver={handleEditDriver} handleDeleteDriver={handleDeleteDriver} />
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value='licenses'>
|
||||
<h2>Водительские права</h2>
|
||||
<Button onClick={() => setLicenseModalOpened(true)}>Добавить права</Button>
|
||||
<Table striped highlightOnHover withColumnBorders mt="md">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>ФИО</Table.Th>
|
||||
<Table.Th>Серия и номер</Table.Th>
|
||||
<Table.Th>Дата выдачи</Table.Th>
|
||||
<Table.Th>Срок действия</Table.Th>
|
||||
<Table.Th>Категории</Table.Th>
|
||||
<Table.Th>Действия</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{drivers.flatMap((driver) =>
|
||||
driver.license?.map((license: IDriverLicense) => (
|
||||
<DriverLicense license={license} handleDeleteLicense={handleDeleteLicense} />
|
||||
))
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
<Modal opened={modalOpened} onClose={() => setModalOpened(false)} title="Добавить / Редактировать водителя">
|
||||
<form onSubmit={driverForm.onSubmit(handleAddDriver)}>
|
||||
|
||||
37
src/utils/date.ts
Normal file
37
src/utils/date.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { DateValue } from "@mantine/dates";
|
||||
import dayjs from "dayjs"
|
||||
|
||||
export function dateToYYYYMMDD(date: DateValue) {
|
||||
if (!date) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0'); // Month is 0-indexed
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
export function formatDDMMYYYY(date: Date): string {
|
||||
return dayjs(date).format("DD.MM.YYYY")
|
||||
}
|
||||
|
||||
export function parseDDMMYYYY(input: string): Date | null {
|
||||
const [dd, mm, yyyy] = input.split(".")
|
||||
const day = parseInt(dd, 10)
|
||||
const month = parseInt(mm, 10)
|
||||
const year = parseInt(yyyy, 10)
|
||||
|
||||
if (
|
||||
isNaN(day) || isNaN(month) || isNaN(year) ||
|
||||
day < 1 || day > 31 ||
|
||||
month < 1 || month > 12 ||
|
||||
year < 1900
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const date = dayjs(`${year}-${month}-${day}`, "YYYY-M-D", true)
|
||||
return date.isValid() ? date.toDate() : null
|
||||
}
|
||||
Reference in New Issue
Block a user