diff --git a/package.json b/package.json index 8317519..17a0fa7 100755 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@tabler/icons-react": "^3.35.0", "axios": "^1.12.2", "better-auth": "^1.3.28", + "dayjs": "^1.11.19", "dotenv": "^17.2.3", "envalid": "^8.1.0", "mysql2": "^3.15.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad59b47..037e178 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,6 +43,9 @@ importers: better-auth: specifier: ^1.3.28 version: 1.3.29(next@15.5.6(@babel/core@7.28.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + dayjs: + specifier: ^1.11.19 + version: 1.11.19 dotenv: specifier: ^17.2.3 version: 17.2.3 @@ -1855,6 +1858,10 @@ packages: { integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== } engines: { node: '>= 0.4' } + dayjs@1.11.19: + resolution: + { integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw== } + debug@3.2.7: resolution: { integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== } @@ -6416,6 +6423,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + dayjs@1.11.19: {} + debug@3.2.7: dependencies: ms: 2.1.3 diff --git a/src/components/atoms/editable-date-cell.tsx b/src/components/atoms/editable-date-cell.tsx new file mode 100644 index 0000000..53fae47 --- /dev/null +++ b/src/components/atoms/editable-date-cell.tsx @@ -0,0 +1,71 @@ +'use client'; + +import dayjs from 'dayjs'; + +type EditableDateCellProperties = { + value: string | null | undefined; + onBlur: (value: string) => void; + disabled?: boolean; + options?: { + dateFormat?: string; + includeTime?: boolean; + }; +}; + +export function EditableDateCell({ value, onBlur, disabled = false, options }: EditableDateCellProperties) { + const includeTime = options?.includeTime ?? false; + + // Convert ISO string to format expected by native inputs + const getInputValue = () => { + if (!value) return ''; + const parsed = dayjs(value); + if (!parsed.isValid()) return ''; + + if (includeTime) { + // datetime-local expects "YYYY-MM-DDTHH:mm" + return parsed.format('YYYY-MM-DDTHH:mm'); + } + // date input expects "YYYY-MM-DD" + return parsed.format('YYYY-MM-DD'); + }; + + const handleChange = (event: React.ChangeEvent) => { + const newValue = event.target.value; + + if (!newValue) { + onBlur(''); + return; + } + + const parsedDate = dayjs(newValue); + if (!parsedDate.isValid()) { + onBlur(newValue); + return; + } + + // Convert to appropriate format + if (includeTime) { + onBlur(parsedDate.toISOString()); + } else { + onBlur(parsedDate.format('YYYY-MM-DD')); + } + }; + + return ( + + ); +} diff --git a/src/components/molecules/column-form-modal.tsx b/src/components/molecules/column-form-modal.tsx index 743ed2a..3b000a7 100644 --- a/src/components/molecules/column-form-modal.tsx +++ b/src/components/molecules/column-form-modal.tsx @@ -1,12 +1,15 @@ -import { Button, Group, Modal, Select, Stack, TextInput } from '@mantine/core'; +import { Button, Checkbox, Group, Modal, Select, Stack, Text, TextInput } from '@mantine/core'; import { useForm } from '@mantine/form'; import { useEffect } from 'react'; import type { Column } from '@/types/schemas/entities/container'; -type ColumnFormValues = { - name: string; - type: 'string' | 'number' | 'boolean'; -}; +const ALLOWED_TYPES = ['string', 'number', 'boolean', 'date'] as const; + +type ColumnFormValues = + | { name: string; type: 'string' } + | { name: string; type: 'number' } + | { name: string; type: 'boolean' } + | { name: string; type: 'date'; options?: { dateFormat?: string; includeTime?: boolean } }; type ColumnFormModalProperties = { opened: boolean; @@ -31,17 +34,27 @@ export function ColumnFormModal({ initialValues: { name: initialValues?.name ?? '', type: initialValues?.type ?? 'string', + ...(initialValues?.type === 'date' && initialValues.options + ? { + options: { + ...(initialValues.options.dateFormat ? { dateFormat: initialValues.options.dateFormat } : {}), + ...(initialValues.options.includeTime === undefined + ? {} + : { includeTime: initialValues.options.includeTime }), + }, + } + : {}), }, validate: { name: (value) => (value.trim() ? null : 'Column name is required'), - type: (value) => { + type: (value: 'string' | 'number' | 'boolean' | 'date') => { if (!value) { return 'Column type is required'; } // TODO move this to a separate enum in the schema definition - const allowedTypes: ('string' | 'number' | 'boolean')[] = ['string', 'number', 'boolean']; - if (!allowedTypes.includes(value)) { - return 'Column type must be one of: string, number, boolean'; + + if (!ALLOWED_TYPES.includes(value)) { + return 'Column type must be one of: string, number, boolean, date'; } return null; }, @@ -53,6 +66,16 @@ export function ColumnFormModal({ form.setValues({ name: initialValues?.name ?? '', type: initialValues?.type ?? 'string', + ...(initialValues?.type === 'date' && initialValues.options + ? { + options: { + ...(initialValues.options.dateFormat ? { dateFormat: initialValues.options.dateFormat } : {}), + ...(initialValues.options.includeTime === undefined + ? {} + : { includeTime: initialValues.options.includeTime }), + }, + } + : {}), }); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -65,7 +88,21 @@ export function ColumnFormModal({ const handleSubmit = async (values: ColumnFormValues) => { try { - await onSubmit(values); + // Only include options for date type, and only if there are values + const submitValues: ColumnFormValues = { + ...values, + ...(values.type === 'date' && + values.options && + (values.options.dateFormat || values.options.includeTime !== undefined) + ? { + options: { + ...(values.options.dateFormat ? { dateFormat: values.options.dateFormat } : {}), + ...(values.options.includeTime === undefined ? {} : { includeTime: values.options.includeTime }), + }, + } + : {}), + }; + await onSubmit(submitValues); handleClose(); } catch (error) { if (onError) { @@ -85,10 +122,30 @@ export function ColumnFormModal({ { value: 'string', label: 'Text' }, { value: 'number', label: 'Number' }, { value: 'boolean', label: 'Checkbox' }, + { value: 'date', label: 'Date' }, ]} {...form.getInputProps('type')} required /> + {form.values.type === 'date' && ( + <> + + Format string using{' '} + + dayjs tokens + {' '} + (optional) + + } + {...form.getInputProps('options.dateFormat')} + /> + + + )}