From bca1e7c2ce7f0d6afe50963bdf54dfbb63fc6472 Mon Sep 17 00:00:00 2001 From: Antonio De Lucreziis Date: Sun, 1 Oct 2023 00:11:38 +0200 Subject: [PATCH] aggiunta view personalizzata per corsi di dottorato ed altre lievi modifiche --- package-lock.json | 24 +- package.json | 1 + server/models/Event.js | 4 +- src/components/PhdCourseLessonList.js | 108 ++++++++ src/models/EventPhdCourse.js | 157 +----------- src/pages/PhdCourseEditPage.js | 344 ++++++++++++++++++++++++++ src/pages/PhdCourseViewPage.js | 65 +++++ 7 files changed, 554 insertions(+), 149 deletions(-) create mode 100644 src/components/PhdCourseLessonList.js create mode 100644 src/pages/PhdCourseEditPage.js create mode 100644 src/pages/PhdCourseViewPage.js diff --git a/package-lock.json b/package-lock.json index ad8a2cab..40d1b709 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dm-manager", - "version": "1.1.20", + "version": "1.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "dm-manager", - "version": "1.1.20", + "version": "1.2.1", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.1.2", "@fortawesome/free-solid-svg-icons": "^6.1.2", @@ -36,6 +36,7 @@ "passport-oauth2": "^1.6.1", "react": "^18.2.0", "react-bootstrap": "^2.5.0", + "react-bootstrap-icons": "^1.10.3", "react-bootstrap-typeahead": "^6.0.0", "react-csv": "^2.2.2", "react-datepicker": "^4.8.0", @@ -15528,6 +15529,17 @@ } } }, + "node_modules/react-bootstrap-icons": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/react-bootstrap-icons/-/react-bootstrap-icons-1.10.3.tgz", + "integrity": "sha512-j4hSby6gT9/enhl3ybB1tfr1slZNAYXDVntcRrmVjxB3//2WwqrzpESVqKhyayYVaWpEtnwf9wgUQ03cuziwrw==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, "node_modules/react-bootstrap-typeahead": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/react-bootstrap-typeahead/-/react-bootstrap-typeahead-6.0.0.tgz", @@ -30042,6 +30054,14 @@ "warning": "^4.0.3" } }, + "react-bootstrap-icons": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/react-bootstrap-icons/-/react-bootstrap-icons-1.10.3.tgz", + "integrity": "sha512-j4hSby6gT9/enhl3ybB1tfr1slZNAYXDVntcRrmVjxB3//2WwqrzpESVqKhyayYVaWpEtnwf9wgUQ03cuziwrw==", + "requires": { + "prop-types": "^15.7.2" + } + }, "react-bootstrap-typeahead": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/react-bootstrap-typeahead/-/react-bootstrap-typeahead-6.0.0.tgz", diff --git a/package.json b/package.json index 97cae182..68be9751 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "passport-oauth2": "^1.6.1", "react": "^18.2.0", "react-bootstrap": "^2.5.0", + "react-bootstrap-icons": "^1.10.3", "react-bootstrap-typeahead": "^6.0.0", "react-csv": "^2.2.2", "react-datepicker": "^4.8.0", diff --git a/server/models/Event.js b/server/models/Event.js index d779b9a8..00f8dd22 100644 --- a/server/models/Event.js +++ b/server/models/Event.js @@ -73,7 +73,7 @@ const EventColloquium = model('EventColloquium', eventColloquiumSchema) // Corso di Dottorato // -const lectureDateSchema = new Schema({ +const lessonSchema = new Schema({ date: { type: Date, label: 'data e orario', required: true }, duration: { type: Number, label: 'durata (in minuti)', default: 120, required: true }, room: { type: ObjectId, label: 'aula', ref: 'Room', required: true }, @@ -82,7 +82,7 @@ const lectureDateSchema = new Schema({ const eventPhdCourseSchema = new Schema({ title: {type: String, label: 'titolo'}, lecturer: { type: ObjectId, label: 'lecturer', ref: 'Person', required: true }, - lectureDates: [lectureDateSchema], + lessons: [lessonSchema], createdBy, updatedBy, diff --git a/src/components/PhdCourseLessonList.js b/src/components/PhdCourseLessonList.js new file mode 100644 index 00000000..3ba562b6 --- /dev/null +++ b/src/components/PhdCourseLessonList.js @@ -0,0 +1,108 @@ +import { Button, Table } from 'react-bootstrap' + +import * as Icon from 'react-bootstrap-icons' +import { Link } from 'react-router-dom'; +import { useEngine } from '../Engine'; +import Loading from './Loading'; + +/** @typedef {import('../models/EventPhdCourse').Lesson} Lesson */ + +/** + * Formats a UTC date to "YYYY-MM-DD HH:mm" + * @type {(date: string | Date) => string} + */ +const formatDate = (date) => { + if (typeof date === 'string') date = new Date(date) + + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const day = date.getDate().toString().padStart(2, '0'); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + + const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}`; + + return formattedDate; +} + +export const isValidDate = (dateString) => { + try { + parseDate(dateString) + return true + } catch (e) { + return false + } +} + +export const parseDate = (dateString) => { + const [datePart, timePart] = dateString.trim().split(/\s+/g) + const [year, month, day] = datePart.split('-') + const [hours, minutes] = timePart.split(':') + + console.log(year, month, day, hours, minutes) + + if (year === undefined || month === undefined || day === undefined || hours === undefined || minutes === undefined) { + throw new Error('invalid date format, expected "YYYY-MM-DD HH:mm"') + } + + const utcDate = new Date( + parseInt(year, 10), + parseInt(month, 10) - 1, // Months are zero-based + parseInt(day, 10), + parseInt(hours, 10), + parseInt(minutes, 10) + ) + + return utcDate +} + +/** + * @param {{ lessons: Lesson[], deleteLesson: (index: number) => void }} props + */ +export const LessonTable = ({ lessons, deleteLesson }) => { + const isEdit = !!deleteLesson + + const Room = ({ id }) => { + const engine = useEngine() + const { isSuccess, data } = engine.useGet('room', id) + + if (!isSuccess) return + const { Room } = engine.Models + return {Room.describe(data)} + } + + return ( + + + + + + + {isEdit && } + + + + {lessons.map((lesson, i) => ( + + + + + {isEdit && ( + + )} + + ))} + +
OrarioDurata (minuti)Stanza
{formatDate(lesson.date)}{lesson.duration} + {typeof lesson.room !== 'string' + ? lesson.room.code + : } + + +
+ ) +} \ No newline at end of file diff --git a/src/models/EventPhdCourse.js b/src/models/EventPhdCourse.js index 5d140db8..9d0707a4 100644 --- a/src/models/EventPhdCourse.js +++ b/src/models/EventPhdCourse.js @@ -1,150 +1,17 @@ -import { useEffect, useState } from 'react' -import { Route, useParams, Navigate } from 'react-router-dom' +import { Route } from 'react-router-dom' import { Link } from 'react-router-dom' -import { Button, ButtonGroup, Card, Container, Form } from 'react-bootstrap' import ModelsPage from '../pages/ModelsPage' -import ModelViewPage from '../pages/ModelViewPage' +import PhdCourseEditPage from '../pages/PhdCourseEditPage' +import PhdCourseViewPage from '../pages/PhdCourseViewPage' - -import Loading from '../components/Loading' -import { useEngine } from '../Engine' -import { ModelHeading } from '../components/ModelHeading' -import { PersonInput, StringInput } from '../components/Input' - -const compareValue = (v1, v2) => { - // capita di confrontare una stringa con una data - if (JSON.stringify(v1) === JSON.stringify(v2)) return true - if (typeof(v1) !== typeof(v2)) return false - if (Array.isArray(v1)) { - if (v1.length !== v2.length) return false - return v1.every((v, i) => compareValue(v, v2[i])) - } - if (typeof(v1) === 'object') return (v1?._id && v1?._id === v2?._id) - return v1 === v2 -} - -const EditPage = ({ Model }) => { - const params = useParams() - const id = params.id - - // const [searchParams] = useSearchParams() - // const clone_id = searchParams.get('clone') - - const [ redirect ] = useState(null) - - const create = id === 'new' - const [modifiedObj, setModifiedObj] = useState(null) - const engine = useEngine() - // const putObj = engine.usePut(objCode) - // const patchObj = engine.usePatch(objCode) - // const engineDeleteObj = engine.useDelete(objCode) - - const { status, data } = engine.useGet(Model.code, id) - - useEffect(() => { - if (status === "success") { - setModifiedObj(data) - } - }, [status, data]) - - if (redirect !== null) return - - if (status === "error") return
errore caricamento
- if (status === "loading") return - if (modifiedObj === null) return - - const originalObj = data - const modifiedFields = Object.keys(modifiedObj) - .filter(key => !compareValue(modifiedObj[key], originalObj[key])) - const changed = modifiedFields.length > 0 - - // const TitleField = withProps(modifiedObj, setModifiedObj, StringInput, 'title') - // const LecturerField = withProps(modifiedObj, setModifiedObj, PersonInput, 'lecturer') - - return ( - <> - - - -

- {create - ? "Nuovo Corso di Dottorato" - : `Modifica Corso di Dottorato ${Model.describe(modifiedObj)}`} -

-
- -
e.preventDefault()}> - - - Titolo - -
- { - setModifiedObj(obj => ({ - ...obj, - title: value, - })) - }} - /> -
-
-
- - - Docente - -
- { - setModifiedObj(obj => ({ - ...obj, - lecturer: value, - })) - }} - /> -
-
-
- - - Lezioni - -
- - - Tabella degli orari... - -
-
- - - - {!create && ( - - )} - -
-
-
- - ) -} +/** + * @typedef {{ + * date: Date, + * duration: number, + * room: Room + * }} Lesson + */ export default class EventPhdCourse { constructor() { @@ -169,8 +36,8 @@ export default class EventPhdCourse { this.schema = null this.IndexPage = ModelsPage - this.ViewPage = ModelViewPage - this.EditPage = EditPage + this.ViewPage = PhdCourseViewPage + this.EditPage = PhdCourseEditPage } // absolute url of objects index diff --git a/src/pages/PhdCourseEditPage.js b/src/pages/PhdCourseEditPage.js new file mode 100644 index 00000000..162fc1a9 --- /dev/null +++ b/src/pages/PhdCourseEditPage.js @@ -0,0 +1,344 @@ +import { useEffect, useState } from 'react' +import { useParams, Navigate, useSearchParams } from 'react-router-dom' +import { Button, ButtonGroup, Card, Container, Form } from 'react-bootstrap' + +import Loading from '../components/Loading' +import { useEngine } from '../Engine' +import { ModelHeading } from '../components/ModelHeading' +import { NumberInput, PersonInput, RoomInput, StringInput } from '../components/Input' +import moment from 'moment' +import { isValidDate, LessonTable, parseDate } from '../components/PhdCourseLessonList' + +/** @typedef {import('../models/EventPhdCourse').Lesson} Lesson */ + +const sortBy = (list, compareFn) => { + const clone = [...list] + clone.sort((a, b) => compareFn(a, b)) + return clone +} + +const compareValue = (v1, v2) => { + // capita di confrontare una stringa con una data + if (JSON.stringify(v1) === JSON.stringify(v2)) return true + if (typeof(v1) !== typeof(v2)) return false + if (Array.isArray(v1)) { + if (v1.length !== v2.length) return false + return v1.every((v, i) => compareValue(v, v2[i])) + } + if (typeof(v1) === 'object') return (v1?._id && v1?._id === v2?._id) + return v1 === v2 +} + +/** + * @type {Record void, lesson: Lesson, count: number?) => void} + */ +const CADENCE_TEMPLATE_GENERATORS = { + 'single': (addLesson, lesson) => { + addLesson(lesson) + }, + 'weekly-1': (addLesson, lesson, count) => { + for (let i = 0; i < count; i++) { + addLesson({ ...lesson, date: moment(lesson.date).add(i, 'weeks').toDate() }) + } + }, + 'weekly-2': (addLesson, lesson, count) => { + for (let i = 0; i < count; i++) { + addLesson({ ...lesson, date: moment(lesson.date).add(i * 2, 'weeks').toDate() }) + } + }, + 'monthly': (addLesson, lesson, count) => { + for (let i = 0; i < count; i++) { + addLesson({ ...lesson, date: moment(lesson.date).add(i, 'months').toDate() }) + } + }, +} + +const GenerateLessonForm = ({ addLesson, close, ...rest }) => { + const [dateTime, setDateTime] = useState('') + const [duration, setDuration] = useState(120) + const [room, setRoom] = useState(null) + + const [cadence, setCadence] = useState('single') + const [repetitions, setRepetitions] = useState(1) + + const handleGenerateLessons = () => { + const date = parseDate(dateTime) + + const baseLesson = { date, duration, room: room._id } + CADENCE_TEMPLATE_GENERATORS[cadence]?.(addLesson, baseLesson, repetitions) + + close() + } + + return ( + + + + Orario + +
+ setDateTime(e.target.value)} + placeholder="YYYY-MM-DD HH:mm" + /> +
+
+ + + Durata + +
+ +
+
+ + + Aula + +
+ +
+
+ + + Cadenza + +
+ +
+
+ {cadence !== 'single' && ( + + + N° Ripetizioni + +
+ +
+
+ )} + + + + +
+ ) +} + +export default ({ Model }) => { + const params = useParams() + const id = params.id + + const [searchParams] = useSearchParams() + const clone_id = searchParams.get('clone') + + const [redirect, setRedirect] = useState(null) + + const create = id === 'new' + const [modifiedObj, setModifiedObj] = useState(null) + const engine = useEngine() + const putObj = engine.usePut(Model.code) + const patchObj = engine.usePatch(Model.code) + const engineDeleteObj = engine.useDelete(Model.code) + + const { status, data } = engine.useGet(Model.code, id) + const { status: cloneStatus, data: cloneData } = engine.useGet(Model.code, clone_id ?? null) + + useEffect(() => { + if (clone_id) { + if (cloneStatus === "success") { + setModifiedObj(cloneData) + } + } else { + if (status === "success") { + setModifiedObj(data) + } + } + }, [status, cloneStatus, data]) + + const [showGenerateLessonForm, setShowGenerateLessonForm] = useState(false) + + if (redirect !== null) return + + if (status === "error") return
errore caricamento
+ if (status === "loading") return + if (modifiedObj === null) return + + const originalObj = data + const modifiedFields = Object.keys(modifiedObj) + .filter(key => !compareValue(modifiedObj[key], originalObj[key])) + const changed = modifiedFields.length > 0 + + const deleteLesson = (index) => { + setModifiedObj(obj => ({ + ...obj, + lessons: [ + ...obj.lessons.slice(0, index), + ...obj.lessons.slice(index + 1), + ] + })) + } + + const addLesson = lesson => { + setModifiedObj(obj => ({ + ...obj, + lessons: sortBy( + [ + ...obj.lessons, + lesson, + ], + (l1, l2) => new Date(l1.date) - new Date(l2.date), + ) + })) + } + + const submit = async () => { + if (modifiedObj._id) { + await patchObj(modifiedObj) + engine.addInfoMessage(`Corso di dottorato modificato`) + setRedirect(Model.viewUrl(originalObj._id)) + } else { + const resultObj = await putObj(modifiedObj) + engine.addInfoMessage(`Nuovo corso di dottorato "${Model.describe(resultObj)}" inserito`) + setRedirect(Model.viewUrl(resultObj._id)) + } + } + + const deletePhdCourse = async () => { + await engineDeleteObj(originalObj) + engine.addWarningMessage(`Corso di Dottorato ${Model.describe(originalObj)} eliminato`) + setRedirect(Model.indexUrl()) + } + + return ( + <> + + + +

+ {create + ? "Nuovo Corso di Dottorato" + : `Modifica Corso di Dottorato ${Model.describe(modifiedObj)}`} +

+
+ +
e.preventDefault()}> + + + Titolo + +
+ { + setModifiedObj(obj => ({ + ...obj, + title: value, + })) + }} + /> +
+
+
+ + + Docente + +
+ { + setModifiedObj(obj => ({ + ...obj, + lecturer: value, + })) + }} + /> +
+
+
+ + + Lezioni + +
+ {!showGenerateLessonForm + ? ( + + ) : ( + setShowGenerateLessonForm(false)} /> + )} + + + +
+
+ + + + {!create && ( + + )} + +
+
+
+ + ) +} diff --git a/src/pages/PhdCourseViewPage.js b/src/pages/PhdCourseViewPage.js new file mode 100644 index 00000000..bb10939d --- /dev/null +++ b/src/pages/PhdCourseViewPage.js @@ -0,0 +1,65 @@ +import { useParams } from 'react-router-dom' +import { ModelHeading } from '../components/ModelHeading' + +import { ObjectProvider, useObject } from '../components/ObjectProvider' +// import RelatedDetails from '../components/RelatedDetails' + +import { Button, ButtonGroup, Card, Container } from 'react-bootstrap' +import { useNavigate } from 'react-router-dom' + +import Timestamps from '../components/Timestamps' +import { ModelFieldOutput } from '../components/ModelOutput' +import { LessonTable } from '../components/PhdCourseLessonList' + +const PhdCourseView = ({ Model }) => { + const obj = useObject() + const navigate = useNavigate() + + const schema = Model.schema.fields + + return ( + + +

{Model.name} {Model.describe(obj)}

+
+ +

+ titolo: + +

+

+ docente: + +

+

Lezioni

+ + + + + + + +
+ + + +
+ ) +} + +export default function PhdCourseViewPage({ Model }) { + const params = useParams() + const id = params.id + + return <> + + + + + +} +