MaskedDateInput: Pass maxDate into DatePicker; Add interfaces for Driver, DriverLicense, DriverCategory

This commit is contained in:
popovspiridon99
2025-07-23 09:38:02 +09:00
parent 2276a97ca6
commit 65201ee646
3 changed files with 105 additions and 142 deletions

View File

@ -1,45 +1,42 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react"
import { import { InputBase, ActionIcon, Popover } from "@mantine/core"
InputBase, import { IconCalendar } from "@tabler/icons-react"
ActionIcon, import { DatePicker } from "@mantine/dates"
Popover, import dayjs from "dayjs"
} from "@mantine/core";
import { IconCalendar } from "@tabler/icons-react";
import { DatePicker } from "@mantine/dates";
import dayjs from "dayjs";
interface Props { interface MaskedDateInputProps {
value?: Date | null; value?: Date | null
onChange: (value: Date | null) => void; onChange: (value: Date | null) => void
label?: string; label?: string
required?: boolean; required?: boolean
placeholder?: string; placeholder?: string
error?: string; error?: string
maxDate?: Date | undefined
} }
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear()
function parseDDMMYYYY(input: string): Date | null { function parseDDMMYYYY(input: string): Date | null {
const [dd, mm, yyyy] = input.split("."); const [dd, mm, yyyy] = input.split(".")
const day = parseInt(dd, 10); const day = parseInt(dd, 10)
const month = parseInt(mm, 10); const month = parseInt(mm, 10)
const year = parseInt(yyyy, 10); const year = parseInt(yyyy, 10)
if ( if (
isNaN(day) || isNaN(month) || isNaN(year) || isNaN(day) || isNaN(month) || isNaN(year) ||
day < 1 || day > 31 || day < 1 || day > 31 ||
month < 1 || month > 12 || month < 1 || month > 12 ||
year < 1900 || year > currentYear year < 1900 //|| year > currentYear
) { ) {
return null; return null
} }
const date = dayjs(`${year}-${month}-${day}`, "YYYY-M-D", true); const date = dayjs(`${year}-${month}-${day}`, "YYYY-M-D", true)
return date.isValid() ? date.toDate() : null; return date.isValid() ? date.toDate() : null
} }
function formatDDMMYYYY(date: Date): string { function formatDDMMYYYY(date: Date): string {
return dayjs(date).format("DD.MM.YYYY"); return dayjs(date).format("DD.MM.YYYY")
} }
export default function MaskedDateInput({ export default function MaskedDateInput({
@ -49,74 +46,46 @@ export default function MaskedDateInput({
required, required,
placeholder = "ДД.MM.ГГГГ", placeholder = "ДД.MM.ГГГГ",
error, error,
}: Props) { maxDate
const [inputValue, setInputValue] = useState(value ? formatDDMMYYYY(value) : ""); }: MaskedDateInputProps) {
const [opened, setOpened] = useState(false); const [inputValue, setInputValue] = useState(value ? formatDDMMYYYY(value) : "")
const inputRef = useRef<HTMLInputElement>(null); const [opened, setOpened] = useState(false)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => { useEffect(() => {
if (value) { if (value) {
const formatted = formatDDMMYYYY(value); const formatted = formatDDMMYYYY(value);
if (formatted !== inputValue) { if (formatted !== inputValue) {
setInputValue(formatted); setInputValue(formatted)
} }
} }
}, [value]); }, [value])
const correctDateString = (raw: string): string => {
const nums = raw.replace(/\D/g, "").slice(0, 8);
const parts: string[] = [];
if (nums.length >= 2) {
const day = Math.min(parseInt(nums.slice(0, 2)), 31);
parts.push(day.toString().padStart(2, "0"));
} else if (nums.length >= 1) {
parts.push(nums.slice(0, 1));
}
if (nums.length >= 4) {
const month = Math.min(parseInt(nums.slice(2, 4)), 12);
parts.push(month.toString().padStart(2, "0"));
} else if (nums.length >= 3) {
parts.push(nums.slice(2, 3));
}
if (nums.length >= 8) {
const yearRaw = parseInt(nums.slice(4, 8));
const year = Math.min(Math.max(1900, yearRaw), currentYear);
parts.push(year.toString());
} else if (nums.length >= 5) {
parts.push(nums.slice(4));
}
return parts.join(".");
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let raw = e.target.value.replace(/[^\d]/g, '').slice(0, 8); let raw = e.target.value.replace(/[^\d]/g, '').slice(0, 8)
let day = raw.slice(0, 2); let day = raw.slice(0, 2)
let month = raw.slice(2, 4); let month = raw.slice(2, 4)
let year = raw.slice(4, 8); let year = raw.slice(4, 8)
if (day.length === 1 && parseInt(day) > 3) day = '0' + day; if (day.length === 1 && parseInt(day) > 3) day = '0' + day
if (month.length === 1 && parseInt(month) > 1) month = '0' + month; if (month.length === 1 && parseInt(month) > 1) month = '0' + month
const newValue = [day, month, year].filter(Boolean).join('.'); const newValue = [day, month, year].filter(Boolean).join('.')
setInputValue(newValue); setInputValue(newValue)
const parsed = parseDDMMYYYY(newValue); const parsed = parseDDMMYYYY(newValue)
if (parsed) onChange?.(parsed); if (parsed) onChange?.(parsed)
}; };
const handleDatePick = (date: Date | null) => { const handleDatePick = (date: Date | null) => {
if (!date) return; if (!date) return
const formatted = formatDDMMYYYY(date); const formatted = formatDDMMYYYY(date)
setInputValue(formatted); setInputValue(formatted)
onChange(date); onChange(date)
setOpened(false); setOpened(false)
}; }
return ( return (
<InputBase <InputBase
@ -152,11 +121,11 @@ export default function MaskedDateInput({
value={value} value={value}
onChange={handleDatePick} onChange={handleDatePick}
defaultLevel="month" defaultLevel="month"
maxDate={new Date()} maxDate={maxDate}
/> />
</Popover.Dropdown> </Popover.Dropdown>
</Popover> </Popover>
} }
/> />
); )
} }

22
src/interfaces/Driver.ts Normal file
View File

@ -0,0 +1,22 @@
export interface IDriver {
id: number
fullname: string
snils: string
birthday: string
iin: string
}
export interface IDriverLicense {
id: number
series_number: string
form_date: string
to_date: string
is_actual: boolean
categories: IDriverLicenseCategory[]
}
export interface IDriverLicenseCategory {
id: number
name: string
name_short: string
}

View File

@ -1,32 +1,20 @@
import 'dayjs/locale/ru'; import 'dayjs/locale/ru'
import { useState, useEffect } from "react"; import { useState, useEffect } from "react"
import { import { Button, TextInput, Table, ActionIcon, Modal, Checkbox } from "@mantine/core"
Button, import { DateValue } from "@mantine/dates"
TextInput, import { useForm } from "@mantine/form"
Table, import { IconEdit, IconTrash } from "@tabler/icons-react"
ActionIcon, import { fetchDictionary, createDictionaryItem, updateDictionaryItem, deleteDictionaryItem } from "../api/api"
Modal, import MaskedDateInput from '../components/MaskedDateInput'
Checkbox, import { IDriver, IDriverLicense } from '../interfaces/Driver'
} from "@mantine/core";
import { DateInput } from "@mantine/dates";
import { useForm } from "@mantine/form";
import { IconEdit, IconTrash } from "@tabler/icons-react";
import {
fetchDictionary,
createDictionaryItem,
updateDictionaryItem,
deleteDictionaryItem,
} from "../api/api";
import MaskedDateInput from '../components/MaskedDateInput';
const DriverForm = () => { const DriverForm = () => {
const [drivers, setDrivers] = useState<any[]>([]); const [drivers, setDrivers] = useState<any[]>([])
const [licenseCategories, setLicenseCategories] = useState<{ value: string; label: string }[]>([]); const [licenseCategories, setLicenseCategories] = useState<{ value: string; label: string }[]>([])
const [selectedDriver, setSelectedDriver] = useState<any | null>(null); const [selectedDriver, setSelectedDriver] = useState<any | null>(null)
const [selectedLicense, setSelectedLicense] = useState<any | null>(null); const [selectedLicense, setSelectedLicense] = useState<any | null>(null)
const [modalOpened, setModalOpened] = useState(false); const [modalOpened, setModalOpened] = useState(false)
const [licenseModalOpened, setLicenseModalOpened] = useState(false); const [licenseModalOpened, setLicenseModalOpened] = useState(false)
const [dateValue, setDateValue] = useState('');
useEffect(() => { useEffect(() => {
fetchDictionary("driver_license_category").then((data) => { fetchDictionary("driver_license_category").then((data) => {
@ -37,14 +25,14 @@ const DriverForm = () => {
setLicenseCategories(options); setLicenseCategories(options);
}); });
fetchDictionary("driver").then(setDrivers); fetchDictionary("driver").then(setDrivers)
}, []); }, [])
const driverForm = useForm({ const driverForm = useForm<Partial<IDriver>>({
initialValues: { initialValues: {
fullname: "", fullname: "",
snils: "", snils: "",
birthday: null, birthday: "",
iin: "", iin: "",
}, },
}); });
@ -61,9 +49,9 @@ const DriverForm = () => {
}, },
}); });
function dateToYYYYMMDD(date) { function dateToYYYYMMDD(date: DateValue) {
if (!(date instanceof Date) || isNaN(date)) { if (!date) {
return ""; // Handle invalid Date objects return "";
} }
const year = date.getFullYear(); const year = date.getFullYear();
@ -114,17 +102,17 @@ const DriverForm = () => {
driver.id === values.driverId driver.id === values.driverId
? { ? {
...driver, ...driver,
license: driver.license.map((lic) => (lic.id === selectedLicense.id ? updatedLicense : lic)), license: driver.license.map((lic: IDriverLicense) => (lic.id === selectedLicense.id ? updatedLicense : lic)),
} }
: driver : driver
) )
); );
} else { } else {
const newLicense = await createDictionaryItem("driver_license", values); const newLicense = await createDictionaryItem("driver_license", values)
const updatedDriver = await updateDictionaryItem("driver_connection", { // const updatedDriver = await updateDictionaryItem("driver_connection", {
driver_id: values.driverId, // driver_id: values.driverId,
driver_license_id: newLicense.id, // driver_license_id: newLicense.id,
}); // })
setDrivers((prev) => setDrivers((prev) =>
prev.map((driver) => prev.map((driver) =>
@ -132,7 +120,7 @@ const DriverForm = () => {
? { ...driver, license: [...(driver.license || []), newLicense] } ? { ...driver, license: [...(driver.license || []), newLicense] }
: driver : driver
) )
); )
} }
setLicenseModalOpened(false); setLicenseModalOpened(false);
@ -144,7 +132,7 @@ const DriverForm = () => {
await deleteDictionaryItem("driver_license", licenseId); await deleteDictionaryItem("driver_license", licenseId);
setDrivers((prev) => setDrivers((prev) =>
prev.map((driver) => prev.map((driver) =>
driver.id === driverId ? { ...driver, license: driver.license.filter((lic) => lic.id !== licenseId) } : driver driver.id === driverId ? { ...driver, license: driver.license.filter((lic: IDriverLicense) => lic.id !== licenseId) } : driver
) )
); );
}; };
@ -187,30 +175,13 @@ const DriverForm = () => {
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
{driver.license && Array.isArray(driver.license) && driver.license.map(dr => ( {driver.license && Array.isArray(driver.license) && driver.license.map((dr: IDriver) => (
<Table.Tr> <Table.Tr>
<Table.Td>{dr}</Table.Td> <Table.Td>{JSON.stringify(dr)}</Table.Td>
</Table.Tr> </Table.Tr>
))} ))}
</> </>
) )
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>
)
} }
return ( return (
@ -249,7 +220,7 @@ const DriverForm = () => {
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{drivers.flatMap((driver) => {drivers.flatMap((driver) =>
driver.license?.map((license) => ( driver.license?.map((license: IDriverLicense) => (
<Table.Tr key={license.id}> <Table.Tr key={license.id}>
<Table.Td>{driver.fullname}</Table.Td> <Table.Td>{driver.fullname}</Table.Td>
<Table.Td>{license.series_number}</Table.Td> <Table.Td>{license.series_number}</Table.Td>
@ -275,6 +246,7 @@ const DriverForm = () => {
<MaskedDateInput <MaskedDateInput
label="Дата рождения" label="Дата рождения"
required required
maxDate={new Date()}
{...driverForm.getInputProps("birthday")} {...driverForm.getInputProps("birthday")}
/> />
<Button fullWidth mt="md" type="submit"> <Button fullWidth mt="md" type="submit">