From 52ac8f2a439764c81d26b297ff29b02493439709 Mon Sep 17 00:00:00 2001 From: marina-bul Date: Thu, 9 Oct 2025 07:49:32 +0300 Subject: [PATCH 1/7] feat(19): Init AddLesson form --- packages/features.lesson.add/README.md | 30 +++++ packages/features.lesson.add/eslint.config.js | 3 + packages/features.lesson.add/index.ts | 1 + packages/features.lesson.add/package.json | 53 ++++++++ .../features.lesson.add/src/hooks/index.ts | 2 + .../src/hooks/useInvoiceForm.ts | 64 ++++++++++ packages/features.lesson.add/src/index.ts | 3 + .../src/model/formSchema.ts | 49 ++++++++ .../features.lesson.add/src/model/index.ts | 1 + .../src/types/InvoiceTypes.ts | 25 ++++ .../src/ui/AddingLessonModal.tsx | 115 ++++++++++++++++++ .../src/ui/StudentSelector.tsx | 45 +++++++ packages/features.lesson.add/tsconfig.json | 7 ++ pnpm-lock.yaml | 114 +++++++++++++++-- 14 files changed, 505 insertions(+), 7 deletions(-) create mode 100644 packages/features.lesson.add/README.md create mode 100644 packages/features.lesson.add/eslint.config.js create mode 100644 packages/features.lesson.add/index.ts create mode 100644 packages/features.lesson.add/package.json create mode 100644 packages/features.lesson.add/src/hooks/index.ts create mode 100644 packages/features.lesson.add/src/hooks/useInvoiceForm.ts create mode 100644 packages/features.lesson.add/src/index.ts create mode 100644 packages/features.lesson.add/src/model/formSchema.ts create mode 100644 packages/features.lesson.add/src/model/index.ts create mode 100644 packages/features.lesson.add/src/types/InvoiceTypes.ts create mode 100644 packages/features.lesson.add/src/ui/AddingLessonModal.tsx create mode 100644 packages/features.lesson.add/src/ui/StudentSelector.tsx create mode 100644 packages/features.lesson.add/tsconfig.json diff --git a/packages/features.lesson.add/README.md b/packages/features.lesson.add/README.md new file mode 100644 index 00000000..9d07398e --- /dev/null +++ b/packages/features.lesson.add/README.md @@ -0,0 +1,30 @@ +# Модуль создания счетов на оплату (features.invoice) + +Модуль предназначен для создания и отправки счетов на оплату в приложении. + +## Структура модуля + +``` +src/ +├── hooks/ - хуки для работы с формой счета и утилитарные хуки +├── model/ - схемы и вспомогательные функции для формы счета +├── types/ - типы TypeScript для модуля +├── ui/ - React-компоненты UI (InvoiceModal, InputWithHelper и др.) +├── locales/ - файлы локализации +``` + +## Использование + +```tsx +import { InvoiceModal } from 'features.invoice'; + +export default function Page() { + const [open, setOpen] = useState(false); + return ( + <> + + + + ); +} +``` diff --git a/packages/features.lesson.add/eslint.config.js b/packages/features.lesson.add/eslint.config.js new file mode 100644 index 00000000..15768bd7 --- /dev/null +++ b/packages/features.lesson.add/eslint.config.js @@ -0,0 +1,3 @@ +import config from 'common.eslint'; + +export default config; diff --git a/packages/features.lesson.add/index.ts b/packages/features.lesson.add/index.ts new file mode 100644 index 00000000..148fc8ed --- /dev/null +++ b/packages/features.lesson.add/index.ts @@ -0,0 +1 @@ +export { AddingLessonModal } from './src'; diff --git a/packages/features.lesson.add/package.json b/packages/features.lesson.add/package.json new file mode 100644 index 00000000..bfec3d57 --- /dev/null +++ b/packages/features.lesson.add/package.json @@ -0,0 +1,53 @@ +{ + "name": "features.lesson.add", + "version": "0.0.0", + "type": "module", + "exports": { + ".": "./index.ts" + }, + "license": "MIT", + "scripts": { + "dev": "tsc --watch", + "lint": "eslint \"**/*.{ts,tsx}\"" + }, + "dependencies": { + "@tanstack/react-router": "1.120.11", + "@tanstack/react-query": "^5.73.3", + "sonner": "^1.4.0", + "common.services": "*", + "common.utils": "*", + "common.config": "*", + "common.api": "*", + "common.env": "*", + "common.types": "*", + "@xipkg/modal": "4.1.0", + "@xipkg/select": "2.2.5", + "@xipkg/utils": "1.8.0", + "@xipkg/form": "4.2.1", + "@xipkg/button": "3.2.0", + "@xipkg/input": "2.2.9", + "@xipkg/icons": "^2.5.4" + }, + "devDependencies": { + "@eslint/js": "^9.19.0", + "common.typescript": "*", + "common.eslint": "*", + "@types/node": "^20.3.1", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "@xipkg/eslint": "3.2.0", + "@xipkg/tailwind": "0.8.1", + "@xipkg/typescript": "latest", + "eslint": "^9.19.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.18", + "globals": "^15.14.0", + "typescript": "~5.7.2", + "typescript-eslint": "^8.22.0" + }, + "peerDependencies": { + "react": "19" + }, + "description": "adding lesson feature", + "author": "xi.effect" +} diff --git a/packages/features.lesson.add/src/hooks/index.ts b/packages/features.lesson.add/src/hooks/index.ts new file mode 100644 index 00000000..6b103bfa --- /dev/null +++ b/packages/features.lesson.add/src/hooks/index.ts @@ -0,0 +1,2 @@ +export { useInvoiceForm } from './useInvoiceForm'; +export { useCreateInvoice } from './useCreateInvoice'; diff --git a/packages/features.lesson.add/src/hooks/useInvoiceForm.ts b/packages/features.lesson.add/src/hooks/useInvoiceForm.ts new file mode 100644 index 00000000..d93c86ca --- /dev/null +++ b/packages/features.lesson.add/src/hooks/useInvoiceForm.ts @@ -0,0 +1,64 @@ +import { useForm } from '@xipkg/form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { formSchema, type FormData } from '../model/formSchema'; +import { useFetchClassrooms } from 'common.services'; + +const DEFAULT_VALUES: FormData = { + title: '', + description: '', + studentId: '', + startTime: '09:00', + endTime: '10:00', + startDate: new Date().toLocaleDateString('ru-RU'), + shouldRepeat: 'dont_repeat', +}; + +export const useInvoiceForm = () => { + const { data: classrooms } = useFetchClassrooms(); + + const form = useForm({ + resolver: zodResolver(formSchema), + mode: 'onSubmit', + defaultValues: DEFAULT_VALUES, + }); + + const { control, handleSubmit, setValue, formState } = form; + + console.log('errors', formState.errors); + + const onSubmit = (data: FormData) => { + const student = classrooms?.find((c) => c.id === Number(data.studentId)); + + const student_ids = student?.kind === 'individual' ? [student.student_id] : []; + + const payload = { + title: data.title, + description: data.description || '', + student_ids, + startTime: data.startTime, + endTime: data.endTime, + startDate: data.startDate, + shouldRepeat: data.shouldRepeat, + }; + + console.log('payload', payload); + }; + + const handleClearForm = () => { + setValue('title', DEFAULT_VALUES.title); + setValue('description', DEFAULT_VALUES.description); + setValue('studentId', DEFAULT_VALUES.studentId); + setValue('startTime', DEFAULT_VALUES.startTime); + setValue('endTime', DEFAULT_VALUES.endTime); + setValue('startDate', DEFAULT_VALUES.startDate); + setValue('shouldRepeat', DEFAULT_VALUES.shouldRepeat); + }; + + return { + form, + control, + handleSubmit, + onSubmit, + handleClearForm, + }; +}; diff --git a/packages/features.lesson.add/src/index.ts b/packages/features.lesson.add/src/index.ts new file mode 100644 index 00000000..276519d6 --- /dev/null +++ b/packages/features.lesson.add/src/index.ts @@ -0,0 +1,3 @@ +export { AddingLessonModal } from './ui/AddingLessonModal'; +export * from './hooks'; +export * from './model'; diff --git a/packages/features.lesson.add/src/model/formSchema.ts b/packages/features.lesson.add/src/model/formSchema.ts new file mode 100644 index 00000000..da7baa12 --- /dev/null +++ b/packages/features.lesson.add/src/model/formSchema.ts @@ -0,0 +1,49 @@ +import * as z from 'zod'; + +const timeToMinutes = (time: string): number => { + const [hours, minutes] = time.split(':').map(Number); + return hours * 60 + minutes; +}; + +// Валидация времени +const timeValidation = z.string().refine((time) => { + const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/; + return timeRegex.test(time); +}, 'Неверный формат времени'); + +export const formSchema = z.object({ + title: z.string(), + description: z.string().optional(), + studentId: z.string().min(1, 'Выберите'), + startTime: timeValidation, + endTime: timeValidation, + startDate: z + .string() + .min(1, 'Укажите дату') + .regex(/^\d{2}\.\d{2}\.\d{4}$/, 'Формат даты: дд.мм.гггг'), + shouldRepeat: z + .enum([ + 'dont_repeat', + 'every_day', + 'every_work_day', + 'every_week', + 'every_2_weeks', + 'every_month', + ]) + .default('dont_repeat'), +}).refine( + (data) => { + if (data.startTime && data.endTime) { + const startMinutes = timeToMinutes(data.startTime); + const endMinutes = timeToMinutes(data.endTime); + return startMinutes <= endMinutes; + } + return true; + }, + { + message: 'Время начала не может быть позже времени окончания', + path: ['startTime'], + }, +); + +export type FormData = z.infer; diff --git a/packages/features.lesson.add/src/model/index.ts b/packages/features.lesson.add/src/model/index.ts new file mode 100644 index 00000000..5d5fd593 --- /dev/null +++ b/packages/features.lesson.add/src/model/index.ts @@ -0,0 +1 @@ +export { formSchema, type FormData } from './formSchema'; diff --git a/packages/features.lesson.add/src/types/InvoiceTypes.ts b/packages/features.lesson.add/src/types/InvoiceTypes.ts new file mode 100644 index 00000000..7bb50701 --- /dev/null +++ b/packages/features.lesson.add/src/types/InvoiceTypes.ts @@ -0,0 +1,25 @@ +export type StudentT = { + id: string; + name: string; + subjects: SubjectT[]; +}; + +export type SubjectT = { + id: string; + name: string; + variant: string; + pricePerLesson: number; + unpaidLessonsAmount?: number; +}; + +export interface CreateInvoicePayload { + invoice: { + comment: string; + }; + items: Array<{ + name: string; + price: number; + quantity: number; + }>; + student_ids: number[]; +} diff --git a/packages/features.lesson.add/src/ui/AddingLessonModal.tsx b/packages/features.lesson.add/src/ui/AddingLessonModal.tsx new file mode 100644 index 00000000..55657440 --- /dev/null +++ b/packages/features.lesson.add/src/ui/AddingLessonModal.tsx @@ -0,0 +1,115 @@ +import { + Modal, + ModalContent, + ModalHeader, + ModalTitle, + ModalFooter, + ModalCloseButton, +} from '@xipkg/modal'; +import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@xipkg/form'; +import { Button } from '@xipkg/button'; +import { Close } from '@xipkg/icons'; +import { Input } from '@xipkg/input'; +import { useInvoiceForm } from '../hooks'; +import type { FormData } from '../model'; +import { StudentSelector } from './StudentSelector'; + +type AddingLessonModalProps = { + open: boolean; + onOpenChange: (open: boolean) => void; +}; + +export const AddingLessonModal = ({ open, onOpenChange }: AddingLessonModalProps) => { + const { + form, + control, + handleSubmit, + handleClearForm, + onSubmit, + } = useInvoiceForm(); + + + const handleCloseModal = () => { + handleClearForm(); + onOpenChange(false); + }; + + const onFormSubmit = (data: FormData) => { + onSubmit(data); + handleCloseModal(); + }; + + + return ( + + + + + +
+
+
Вторник, 23 сентября
+

Здесь будет календарь на день

+
+
+ + Назначение занятия + +
+ + ( + + Название + + + + + + )} + /> + ( + + Описание + + + + + + )} + /> + + + + + + + + + +
+ +
+ +
+
+ ); +}; diff --git a/packages/features.lesson.add/src/ui/StudentSelector.tsx b/packages/features.lesson.add/src/ui/StudentSelector.tsx new file mode 100644 index 00000000..3aeea98e --- /dev/null +++ b/packages/features.lesson.add/src/ui/StudentSelector.tsx @@ -0,0 +1,45 @@ +import { FormControl, FormField, FormItem, FormLabel } from '@xipkg/form'; +import { Select, SelectValue, SelectTrigger, SelectContent, SelectItem } from '@xipkg/select'; +import { useFetchClassrooms } from 'common.services'; + +type StudentSelectorProps = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + control: any; +}; + +export const StudentSelector = ({ control }: StudentSelectorProps) => { + const { data: classrooms, isLoading } = useFetchClassrooms(); + + return ( + ( + + Ученик или группа + + + + + )} + /> + ); +}; diff --git a/packages/features.lesson.add/tsconfig.json b/packages/features.lesson.add/tsconfig.json new file mode 100644 index 00000000..7bc23fd3 --- /dev/null +++ b/packages/features.lesson.add/tsconfig.json @@ -0,0 +1,7 @@ +{ + "include": ["src/**/*"], + "extends": [ + "common.typescript/tsconfig.app.json", + ], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1dc4fcd..e17c391d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1157,6 +1157,106 @@ importers: specifier: ^8.22.0 version: 8.44.0(eslint@9.36.0)(typescript@5.7.3) + packages/features.lesson.add: + dependencies: + '@tanstack/react-query': + specifier: ^5.73.3 + version: 5.73.3(react@19.1.1) + '@tanstack/react-router': + specifier: 1.120.11 + version: 1.120.11(react-dom@19.1.1)(react@19.1.1) + '@xipkg/button': + specifier: 3.2.0 + version: 3.2.0(@types/react@19.1.13)(react@19.1.1) + '@xipkg/form': + specifier: 4.2.1 + version: 4.2.1(@types/react-dom@19.1.9)(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.1) + '@xipkg/icons': + specifier: ^2.5.4 + version: 2.5.4(react@19.1.1) + '@xipkg/input': + specifier: 2.2.9 + version: 2.2.9(react@19.1.1) + '@xipkg/modal': + specifier: 4.1.0 + version: 4.1.0(@types/react-dom@19.1.9)(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.1) + '@xipkg/select': + specifier: 2.2.5 + version: 2.2.5(@types/react-dom@19.1.9)(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.1) + '@xipkg/utils': + specifier: 1.8.0 + version: 1.8.0(react@19.1.1) + common.api: + specifier: '*' + version: link:../common.api + common.config: + specifier: '*' + version: link:../common.config + common.env: + specifier: '*' + version: link:../common.env + common.services: + specifier: '*' + version: link:../common.services + common.types: + specifier: '*' + version: link:../common.types + common.utils: + specifier: '*' + version: link:../common.utils + react: + specifier: '19' + version: 19.1.1 + sonner: + specifier: ^1.4.0 + version: 1.7.4(react-dom@19.1.1)(react@19.1.1) + devDependencies: + '@eslint/js': + specifier: ^9.19.0 + version: 9.36.0 + '@types/node': + specifier: ^20.3.1 + version: 20.19.17 + '@types/react': + specifier: ^19.0.2 + version: 19.1.13 + '@types/react-dom': + specifier: ^19.0.2 + version: 19.1.9(@types/react@19.1.13) + '@xipkg/eslint': + specifier: 3.2.0 + version: 3.2.0(eslint-plugin-jsx-a11y@6.10.2)(eslint@9.36.0)(turbo@2.5.6)(typescript@5.7.3) + '@xipkg/tailwind': + specifier: 0.8.1 + version: 0.8.1 + '@xipkg/typescript': + specifier: latest + version: 0.2.0 + common.eslint: + specifier: '*' + version: link:../common.eslint + common.typescript: + specifier: '*' + version: link:../common.typescript + eslint: + specifier: ^9.19.0 + version: 9.36.0 + eslint-plugin-react-hooks: + specifier: ^5.0.0 + version: 5.2.0(eslint@9.36.0) + eslint-plugin-react-refresh: + specifier: ^0.4.18 + version: 0.4.18(eslint@9.36.0) + globals: + specifier: ^15.14.0 + version: 15.15.0 + typescript: + specifier: ~5.7.2 + version: 5.7.3 + typescript-eslint: + specifier: ^8.22.0 + version: 8.44.0(eslint@9.36.0)(typescript@5.7.3) + packages/features.materials.add: dependencies: '@xipkg/button': @@ -9497,7 +9597,7 @@ packages: eslint-config-airbnb: 19.0.4(eslint-plugin-import@2.31.0)(eslint-plugin-jsx-a11y@6.10.2)(eslint-plugin-react-hooks@5.1.0)(eslint-plugin-react@7.37.2)(eslint@9.36.0) eslint-config-next: 15.1.2(eslint@9.36.0)(typescript@5.7.3) eslint-config-prettier: 9.1.0(eslint@9.36.0) - eslint-config-turbo: 2.5.7(eslint@9.36.0)(turbo@2.5.6) + eslint-config-turbo: 2.5.8(eslint@9.36.0)(turbo@2.5.6) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.44.0)(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0) eslint-plugin-react: 7.37.2(eslint@9.36.0) eslint-plugin-react-hooks: 5.1.0(eslint@9.36.0) @@ -9602,7 +9702,7 @@ packages: peerDependencies: react: ^19 dependencies: - '@radix-ui/react-label': 2.1.2(@types/react-dom@19.1.9)(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.1) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.1.9)(@types/react@19.1.13)(react-dom@19.1.1)(react@19.1.1) '@xipkg/utils': 1.8.0(react@19.1.1) class-variance-authority: 0.7.1 react: 19.1.1 @@ -11026,14 +11126,14 @@ packages: eslint: 9.36.0 dev: true - /eslint-config-turbo@2.5.7(eslint@9.36.0)(turbo@2.5.6): - resolution: {integrity: sha512-6p1qq5I/gO8tkPhZmhO5Ug5VJPV6JX/BPaYanToYm8DwALHW3eC3HHsToeQ6Rq1xaXPFZJNAD27nZVLcr8FQkg==} + /eslint-config-turbo@2.5.8(eslint@9.36.0)(turbo@2.5.6): + resolution: {integrity: sha512-wzxmN7dJNFGDwOvR/4j8U2iaIH/ruYez8qg/sCKrezJ3+ljbFMvJLmgKKt/1mDuyU9wj5aZqO6VijP3QH169FA==} peerDependencies: eslint: '>6.6.0' turbo: '>2.0.0' dependencies: eslint: 9.36.0 - eslint-plugin-turbo: 2.5.7(eslint@9.36.0)(turbo@2.5.6) + eslint-plugin-turbo: 2.5.8(eslint@9.36.0)(turbo@2.5.6) turbo: 2.5.6 dev: true @@ -11260,8 +11360,8 @@ packages: turbo: 2.5.6 dev: true - /eslint-plugin-turbo@2.5.7(eslint@9.36.0)(turbo@2.5.6): - resolution: {integrity: sha512-FXTyV3Lmk5xzut5lpXr6YwYwrvoI7nDveOmSzvePZMgJfZ/zMh5rVTN1xSpL5sWe9LkI7ZYPazj/aUrLZfbZIA==} + /eslint-plugin-turbo@2.5.8(eslint@9.36.0)(turbo@2.5.6): + resolution: {integrity: sha512-bVjx4vTH0oTKIyQ7EGFAXnuhZMrKIfu17qlex/dps7eScPnGQLJ3r1/nFq80l8xA+8oYjsSirSQ2tXOKbz3kEw==} peerDependencies: eslint: '>6.6.0' turbo: '>2.0.0' From 5fc3233886753cbee7e3e2fb3550b339261e0809 Mon Sep 17 00:00:00 2001 From: marina-bul Date: Tue, 21 Oct 2025 09:48:58 +0300 Subject: [PATCH 2/7] feat(19): Add date inputs and calendar block --- apps/xi.web/src/index.css | 1 + .../features.lesson.add/src/hooks/index.ts | 3 +- .../{useInvoiceForm.ts => useAddingForm.ts} | 2 +- .../src/model/formSchema.ts | 70 +- .../src/ui/AddingLessonModal.tsx | 189 +- .../src/ui/DayCalendar.tsx | 152 + .../src/ui/StudentSelector.tsx | 8 +- packages/pages.main/package.json | 1 + packages/pages.main/src/ui/MainPage.tsx | 46 +- .../AssignLessonButton/AssignLessonButton.tsx | 15 +- pnpm-lock.yaml | 4969 ++++++++--------- 11 files changed, 2791 insertions(+), 2665 deletions(-) rename packages/features.lesson.add/src/hooks/{useInvoiceForm.ts => useAddingForm.ts} (97%) create mode 100644 packages/features.lesson.add/src/ui/DayCalendar.tsx diff --git a/apps/xi.web/src/index.css b/apps/xi.web/src/index.css index 40a4a9de..5695395a 100644 --- a/apps/xi.web/src/index.css +++ b/apps/xi.web/src/index.css @@ -32,6 +32,7 @@ @source "../../../packages/pages.invites/src"; @source "../../../packages/pages.reset-password/src"; @source "../../../packages/features.invoice/src"; +@source "../../../packages/features.lesson.add/src"; @source "../../../node_modules/@xipkg"; diff --git a/packages/features.lesson.add/src/hooks/index.ts b/packages/features.lesson.add/src/hooks/index.ts index 6b103bfa..be717e99 100644 --- a/packages/features.lesson.add/src/hooks/index.ts +++ b/packages/features.lesson.add/src/hooks/index.ts @@ -1,2 +1 @@ -export { useInvoiceForm } from './useInvoiceForm'; -export { useCreateInvoice } from './useCreateInvoice'; +export { useAddingForm } from './useAddingForm'; diff --git a/packages/features.lesson.add/src/hooks/useInvoiceForm.ts b/packages/features.lesson.add/src/hooks/useAddingForm.ts similarity index 97% rename from packages/features.lesson.add/src/hooks/useInvoiceForm.ts rename to packages/features.lesson.add/src/hooks/useAddingForm.ts index d93c86ca..fb25442d 100644 --- a/packages/features.lesson.add/src/hooks/useInvoiceForm.ts +++ b/packages/features.lesson.add/src/hooks/useAddingForm.ts @@ -13,7 +13,7 @@ const DEFAULT_VALUES: FormData = { shouldRepeat: 'dont_repeat', }; -export const useInvoiceForm = () => { +export const useAddingForm = () => { const { data: classrooms } = useFetchClassrooms(); const form = useForm({ diff --git a/packages/features.lesson.add/src/model/formSchema.ts b/packages/features.lesson.add/src/model/formSchema.ts index da7baa12..91329c24 100644 --- a/packages/features.lesson.add/src/model/formSchema.ts +++ b/packages/features.lesson.add/src/model/formSchema.ts @@ -11,39 +11,41 @@ const timeValidation = z.string().refine((time) => { return timeRegex.test(time); }, 'Неверный формат времени'); -export const formSchema = z.object({ - title: z.string(), - description: z.string().optional(), - studentId: z.string().min(1, 'Выберите'), - startTime: timeValidation, - endTime: timeValidation, - startDate: z - .string() - .min(1, 'Укажите дату') - .regex(/^\d{2}\.\d{2}\.\d{4}$/, 'Формат даты: дд.мм.гггг'), - shouldRepeat: z - .enum([ - 'dont_repeat', - 'every_day', - 'every_work_day', - 'every_week', - 'every_2_weeks', - 'every_month', - ]) - .default('dont_repeat'), -}).refine( - (data) => { - if (data.startTime && data.endTime) { - const startMinutes = timeToMinutes(data.startTime); - const endMinutes = timeToMinutes(data.endTime); - return startMinutes <= endMinutes; - } - return true; - }, - { - message: 'Время начала не может быть позже времени окончания', - path: ['startTime'], - }, -); +export const formSchema = z + .object({ + title: z.string(), + description: z.string().optional(), + studentId: z.string().min(1, 'Выберите'), + startTime: timeValidation, + endTime: timeValidation, + startDate: z + .string() + .min(1, 'Укажите дату') + .regex(/^\d{2}\.\d{2}\.\d{4}$/, 'Формат даты: дд.мм.гггг'), + shouldRepeat: z + .enum([ + 'dont_repeat', + 'every_day', + 'every_work_day', + 'every_week', + 'every_2_weeks', + 'every_month', + ]) + .default('dont_repeat'), + }) + .refine( + (data) => { + if (data.startTime && data.endTime) { + const startMinutes = timeToMinutes(data.startTime); + const endMinutes = timeToMinutes(data.endTime); + return startMinutes <= endMinutes; + } + return true; + }, + { + message: 'Время начала не может быть позже времени окончания', + path: ['startTime'], + }, + ); export type FormData = z.infer; diff --git a/packages/features.lesson.add/src/ui/AddingLessonModal.tsx b/packages/features.lesson.add/src/ui/AddingLessonModal.tsx index 55657440..bb03b920 100644 --- a/packages/features.lesson.add/src/ui/AddingLessonModal.tsx +++ b/packages/features.lesson.add/src/ui/AddingLessonModal.tsx @@ -1,18 +1,12 @@ -import { - Modal, - ModalContent, - ModalHeader, - ModalTitle, - ModalFooter, - ModalCloseButton, -} from '@xipkg/modal'; +import { Modal, ModalContent, ModalTitle, ModalFooter, ModalCloseButton } from '@xipkg/modal'; import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@xipkg/form'; import { Button } from '@xipkg/button'; import { Close } from '@xipkg/icons'; import { Input } from '@xipkg/input'; -import { useInvoiceForm } from '../hooks'; +import { useAddingForm } from '../hooks'; import type { FormData } from '../model'; import { StudentSelector } from './StudentSelector'; +import { DayCalendar } from './DayCalendar'; type AddingLessonModalProps = { open: boolean; @@ -20,14 +14,7 @@ type AddingLessonModalProps = { }; export const AddingLessonModal = ({ open, onOpenChange }: AddingLessonModalProps) => { - const { - form, - control, - handleSubmit, - handleClearForm, - onSubmit, - } = useInvoiceForm(); - + const { form, control, handleSubmit, handleClearForm, onSubmit } = useAddingForm(); const handleCloseModal = () => { handleClearForm(); @@ -39,76 +26,122 @@ export const AddingLessonModal = ({ open, onOpenChange }: AddingLessonModalProps handleCloseModal(); }; - return ( - + -
-
-
Вторник, 23 сентября
-

Здесь будет календарь на день

+
+
+
-
- - Назначение занятия - -
- - ( - - Название - - - - - - )} - /> - ( - - Описание - - - - - - )} - /> - +
+ Назначение занятия + + +
+ ( + + Название + + + + + + )} + /> + ( + + Описание + + + + + + )} + /> +
+
+ +
+
+
Время
+
+ ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> +
+
- - - - - - - + + + + + +
-
- ); diff --git a/packages/features.lesson.add/src/ui/DayCalendar.tsx b/packages/features.lesson.add/src/ui/DayCalendar.tsx new file mode 100644 index 00000000..61321c08 --- /dev/null +++ b/packages/features.lesson.add/src/ui/DayCalendar.tsx @@ -0,0 +1,152 @@ +import { format } from 'date-fns'; +import { cn } from '@xipkg/utils'; + +import { ScrollArea } from '@xipkg/scrollarea'; + +// Функция для создания даты с сегодняшним днем и фиксированным временем +const createTodayWithTime = (timeString: string) => { + const today = new Date(); + const [hours, minutes] = timeString.split(':').map(Number); + today.setHours(hours, minutes, 0, 0); + return today; +}; + +// Тип для события календаря +interface CalendarEventType { + id: string; + title: string; + start: Date; + end: Date; + type: string; + isAllDay: boolean; + isCancelled?: boolean; +} + +// Простой компонент события для моковых данных +const CalendarEvent = ({ event }: { event: CalendarEventType }) => ( +
+ +); + +const MOCK_EVENTS: CalendarEventType[] = [ + { + id: '1', + title: 'Дмитрий', + start: createTodayWithTime('17:00'), + end: createTodayWithTime('18:00'), + type: 'lesson', + isAllDay: false, + }, + { + id: '2', + title: 'Отдых', + start: new Date(), + end: new Date(), + type: 'rest', + isAllDay: true, + }, + { + id: '3', + title: 'Анна', + start: createTodayWithTime('17:00'), + end: createTodayWithTime('18:00'), + type: 'lesson', + isCancelled: true, + isAllDay: false, + }, + { + id: '5', + title: 'Елена', + start: createTodayWithTime('10:00'), + end: createTodayWithTime('12:00'), + type: 'lesson', + isAllDay: false, + }, +]; + +const hours = Array.from({ length: 24 }, (_, i) => `${String(i).padStart(2, '0')}:00`); + +/** + * Адаптивный компонент календаря «День». + * ─ Первый столбец (метки времени) фиксирован шириной 5 rem. + * ─ На day-view: time-col + 1 день. + * ─ sticky-хедер с названием дня и датой, основная сетка прокручивается по вертикали. + */ +export const DayCalendar = ({ day }: { day: Date }) => { + // Шаблон колонок для CSS grid + const colTemplate = '[grid-template-columns:theme(width.20)_1fr]'; + + return ( +
+ {/* Хедер */} +
+ {format(day, 'd')} {format(day, 'MMMM')} +
+ + {/* Основная прокручиваемая зона */} + +
+ {/* Колонка времени */} +
+ {/* Весь день */} +
+ Весь день +
+ {hours.map((hour, i) => ( +
+ + {i !== 0 && hour} + +
+ ))} +
+ +
+ {/* Секция "Весь день" */} +
+ {MOCK_EVENTS.map( + (event) => event.isAllDay && , + )} +
+ + {/* Слоты часов */} + {hours.map((hour) => ( +
+ {MOCK_EVENTS.map((event) => { + const hourAsNumber = +hour.split(':')[0]; + + return ( + !event.isAllDay && + event.start.getHours() === hourAsNumber && ( + + ) + ); + })} +
+ ))} +
+
+
+
+ ); +}; diff --git a/packages/features.lesson.add/src/ui/StudentSelector.tsx b/packages/features.lesson.add/src/ui/StudentSelector.tsx index 3aeea98e..1aafdc60 100644 --- a/packages/features.lesson.add/src/ui/StudentSelector.tsx +++ b/packages/features.lesson.add/src/ui/StudentSelector.tsx @@ -17,13 +17,11 @@ export const StudentSelector = ({ control }: StudentSelectorProps) => { defaultValue="" render={({ field }) => ( - Ученик или группа + Ученик или группа - - - - )} - /> - ( - - Описание - - - - - - )} - /> -
-
- -
-
-
Время
-
- ( - - - - - - - )} - /> - ( - - - - - - - )} - /> - ( - - - - - - - )} - /> - ( - - - - - - - )} - /> -
-
- - - - - - + + + + + +
diff --git a/packages/features.lesson.add/src/ui/components/AddingForm.tsx b/packages/features.lesson.add/src/ui/components/AddingForm.tsx new file mode 100644 index 00000000..47a2a183 --- /dev/null +++ b/packages/features.lesson.add/src/ui/components/AddingForm.tsx @@ -0,0 +1,139 @@ +import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@xipkg/form'; +import { Input } from '@xipkg/input'; +import { useMaskInput } from '@xipkg/inputmask'; +import { ArrowRight, Clock } from '@xipkg/icons'; +import { useAddingForm } from '../../hooks'; +import { InputDate } from './InputDate'; +import { RepeatBlock } from './RepeatBlock'; +import { StudentSelector } from './StudentSelector'; + +import type { FC, PropsWithChildren } from 'react'; +import type { FormData } from '../../model'; + +interface AddingFormProps extends PropsWithChildren { + onClose: () => void; +} + +export const AddingForm: FC = ({ children, onClose }) => { + const { form, control, handleSubmit, handleClearForm, onSubmit } = useAddingForm(); + + const maskRefStartTime = useMaskInput('time'); + const maskRefEndTime = useMaskInput('time'); + + const handleReset = () => { + handleClearForm(); + onClose(); + }; + + const onFormSubmit = (data: FormData) => { + onSubmit(data); + onClose(); + }; + + return ( +
+ +
+ ( + + Название + + + + + + )} + /> + ( + + Описание + + + + + + )} + /> +
+
+ +
+
+
Время
+
+ ( + + + } + variant="s" + /> + + + + )} + /> + ( + + + } + variant="s" + /> + + + + )} + /> + ( + + + + + + + )} + /> + ( + + + + + + )} + /> +
+
+ {children} +
+ + ); +}; diff --git a/packages/features.lesson.add/src/ui/components/InputDate.tsx b/packages/features.lesson.add/src/ui/components/InputDate.tsx new file mode 100644 index 00000000..6c352e0a --- /dev/null +++ b/packages/features.lesson.add/src/ui/components/InputDate.tsx @@ -0,0 +1,35 @@ +import { memo, useCallback, useEffect, useState } from 'react'; + +import { DatePicker } from '@xipkg/datepicker'; +import { Calendar } from '@xipkg/icons'; +import { Input } from '@xipkg/input'; +import { convertStringToDate, getFullDateString } from '../../utils/utils'; + +interface InputDateProps { + value?: string; +} + +export const InputDate = memo(({ value }) => { + const [date, setDate] = useState(convertStringToDate(value || '')); + + const handleSelectDate = useCallback((newDate: Date) => { + setDate(newDate); + }, []); + + useEffect(() => { + setDate(convertStringToDate(value || '')); + }, [value]); + + return ( + + } + /> + + ); +}); diff --git a/packages/features.lesson.add/src/ui/components/RepeatBlock.tsx b/packages/features.lesson.add/src/ui/components/RepeatBlock.tsx new file mode 100644 index 00000000..05a56f0b --- /dev/null +++ b/packages/features.lesson.add/src/ui/components/RepeatBlock.tsx @@ -0,0 +1,52 @@ +import { useTranslation } from 'react-i18next'; + +import { + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectGroup, + SelectItem, + SelectSeparator, +} from '@xipkg/select'; +import { Redo } from '@xipkg/icons'; +import { useConstants } from '../../hooks'; + +import type { FC } from 'react'; + +interface RepeatBlockProps { + value: string; + onChange: (value: string) => void; +} + +export const RepeatBlock: FC = ({ value, onChange }) => { + const { t } = useTranslation('calendar'); + const { repeatVariants } = useConstants(); + + return ( + + ); +}; diff --git a/packages/features.lesson.add/src/ui/StudentSelector.tsx b/packages/features.lesson.add/src/ui/components/StudentSelector.tsx similarity index 100% rename from packages/features.lesson.add/src/ui/StudentSelector.tsx rename to packages/features.lesson.add/src/ui/components/StudentSelector.tsx diff --git a/packages/features.lesson.add/src/utils/utils.ts b/packages/features.lesson.add/src/utils/utils.ts new file mode 100644 index 00000000..80760ac5 --- /dev/null +++ b/packages/features.lesson.add/src/utils/utils.ts @@ -0,0 +1,12 @@ +import { parse } from 'date-fns'; + +export const getFullDateString = (date: Date) => { + const weekDayName = date.toLocaleDateString('ru-RU', { weekday: 'short' }); + const monthName = date.toLocaleDateString('ru-RU', { month: 'long' }); + + return `${weekDayName} ${date.getDate()} ${monthName}`; +}; + +export const convertStringToDate = (dateString: string): Date => { + return parse(dateString, 'dd.MM.yyyy', new Date()); +}; From 36e5f992360f19e7fc296715866eed00f1d1fd2a Mon Sep 17 00:00:00 2001 From: marina-bul Date: Wed, 12 Nov 2025 23:53:44 +0300 Subject: [PATCH 4/7] fix(19): Fix datepicker, schema and date components --- .../src/hooks/useAddingForm.ts | 6 ++-- .../src/model/formSchema.ts | 5 +--- .../src/ui/AddingLessonModal.tsx | 10 +++++-- .../src/ui/AddingModal.css | 30 +++++++++++++++++++ .../src/ui/DayCalendar.tsx | 3 +- .../src/ui/components/AddingForm.tsx | 15 +++++++--- .../src/ui/components/InputDate.tsx | 25 +++++++++++----- .../src/ui/components/RepeatBlock.tsx | 2 +- .../features.lesson.add/src/utils/utils.ts | 4 +-- pnpm-lock.yaml | 6 ++++ 10 files changed, 81 insertions(+), 25 deletions(-) create mode 100644 packages/features.lesson.add/src/ui/AddingModal.css diff --git a/packages/features.lesson.add/src/hooks/useAddingForm.ts b/packages/features.lesson.add/src/hooks/useAddingForm.ts index fb25442d..806a9ec6 100644 --- a/packages/features.lesson.add/src/hooks/useAddingForm.ts +++ b/packages/features.lesson.add/src/hooks/useAddingForm.ts @@ -9,7 +9,7 @@ const DEFAULT_VALUES: FormData = { studentId: '', startTime: '09:00', endTime: '10:00', - startDate: new Date().toLocaleDateString('ru-RU'), + startDate: new Date(), shouldRepeat: 'dont_repeat', }; @@ -22,7 +22,8 @@ export const useAddingForm = () => { defaultValues: DEFAULT_VALUES, }); - const { control, handleSubmit, setValue, formState } = form; + const { control, handleSubmit, setValue, formState, watch } = form; + const eventDate = watch('startDate'); console.log('errors', formState.errors); @@ -57,6 +58,7 @@ export const useAddingForm = () => { return { form, control, + eventDate, handleSubmit, onSubmit, handleClearForm, diff --git a/packages/features.lesson.add/src/model/formSchema.ts b/packages/features.lesson.add/src/model/formSchema.ts index d53639ea..31fe6878 100644 --- a/packages/features.lesson.add/src/model/formSchema.ts +++ b/packages/features.lesson.add/src/model/formSchema.ts @@ -18,10 +18,7 @@ export const formSchema = z studentId: z.string().min(1, 'Выберите студента'), startTime: timeValidation, endTime: timeValidation, - startDate: z - .string() - .min(1, 'Укажите дату') - .regex(/^\d{2}\.\d{2}\.\d{4}$/, 'Формат даты: дд.мм.гггг'), + startDate: z.date({ required_error: 'Укажите дату' }), shouldRepeat: z .enum([ 'dont_repeat', diff --git a/packages/features.lesson.add/src/ui/AddingLessonModal.tsx b/packages/features.lesson.add/src/ui/AddingLessonModal.tsx index fdf15a7a..3b2d7e0c 100644 --- a/packages/features.lesson.add/src/ui/AddingLessonModal.tsx +++ b/packages/features.lesson.add/src/ui/AddingLessonModal.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { Modal, ModalContent, ModalTitle, ModalFooter, ModalCloseButton } from '@xipkg/modal'; import { Button } from '@xipkg/button'; import { Close } from '@xipkg/icons'; @@ -5,30 +6,33 @@ import { Close } from '@xipkg/icons'; import { DayCalendar } from './DayCalendar'; import { AddingForm } from './components/AddingForm'; +import './AddingModal.css'; + type AddingLessonModalProps = { open: boolean; onOpenChange: (open: boolean) => void; }; export const AddingLessonModal = ({ open, onOpenChange }: AddingLessonModalProps) => { + const [eventDate, setEventDate] = useState(new Date()); const handleCloseModal = () => { onOpenChange(false); }; return ( - +
- +
Назначение занятия - +