diff --git a/backend/app/coordinator/schemas/examination_schedule.py b/backend/app/coordinator/schemas/examination_schedule.py index 4c7ffe2..62fb2b4 100644 --- a/backend/app/coordinator/schemas/examination_schedule.py +++ b/backend/app/coordinator/schemas/examination_schedule.py @@ -34,7 +34,7 @@ class ExaminationSchedulesQuerySchema(Schema): class ProjectSupervisorStatisticsSchema(Schema): - fullname = fields.Str() + full_name = fields.Str() assigned_to_committee = fields.Integer() groups_assigned_to_his_committee = fields.Integer() diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8ba7c30..a277aeb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,10 +18,11 @@ "axios": "^0.27.2", "classnames": "^2.3.1", "daisyui": "^2.15.2", - "luxon": "^3.0.4", + "dayjs": "^1.11.7", "react": "^18.1.0", "react-big-calendar": "^1.5.0", "react-date-picker": "^9.1.0", + "react-datetime-picker": "^4.1.1", "react-dom": "^18.1.0", "react-hook-form": "^7.31.3", "react-modal": "^3.16.1", @@ -34,8 +35,8 @@ "web-vitals": "^2.1.4" }, "devDependencies": { - "@types/luxon": "^3.0.2", "@types/react-big-calendar": "^0.38.2", + "@types/react-datetime-picker": "^3.4.1", "@types/react-modal": "^3.13.1", "autoprefixer": "^10.4.7", "postcss": "^8.4.14", @@ -3823,12 +3824,6 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, - "node_modules/@types/luxon": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.0.2.tgz", - "integrity": "sha512-HM2OVWckUMmXbWYZufmWT2XMURGDZ6XbyNyQ+Lx+gCFGFqbZaIjsz7b+AGeGP/AuVYHBiuGY+wXfweP1RremnA==", - "dev": true - }, "node_modules/@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -3897,6 +3892,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-datetime-picker": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@types/react-datetime-picker/-/react-datetime-picker-3.4.1.tgz", + "integrity": "sha512-JHqB74+8Zq6cY0PTJ6Wi5Pm6qkNUmooyFfW5SiknSY2xJG1UG8+ljyWTZAvgHvj0XpqcWCHqqYUPiAVagnf9Sg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-dom": { "version": "18.0.4", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.4.tgz", @@ -6310,6 +6314,11 @@ "resolved": "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz", "integrity": "sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==" }, + "node_modules/dayjs": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -11565,14 +11574,6 @@ "node": ">=10" } }, - "node_modules/luxon": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.0.4.tgz", - "integrity": "sha512-aV48rGUwP/Vydn8HT+5cdr26YYQiUZ42NM6ToMoaGKwYfWbfLeRkEu1wXWMHBZT6+KyLfcbbtVcoQFCbbPjKlw==", - "engines": { - "node": ">=12" - } - }, "node_modules/lz-string": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", @@ -13914,6 +13915,24 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-clock": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-clock/-/react-clock-4.0.0.tgz", + "integrity": "sha512-CBevN5B40TDUegSWzXk6bSwXhYzyerL9JGTme8GMAY0zO4FiEhVTGN1uzgC0rn/oSAMJw3M5wSf/OJpp9vcN2Q==", + "dependencies": { + "@wojtekmaj/date-utils": "^1.0.0", + "clsx": "^1.2.1", + "get-user-locale": "^1.4.0", + "prop-types": "^15.6.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-clock?sponsor=1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-date-picker": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/react-date-picker/-/react-date-picker-9.1.0.tgz", @@ -13937,6 +13956,30 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-datetime-picker": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/react-datetime-picker/-/react-datetime-picker-4.1.1.tgz", + "integrity": "sha512-e8ANKLcWFL4/TutvggqVfRiDigyelcVdLvWQzOP5cMJ6IxR08Qv2dOncjBWVkwU9vTsDemuHSAGIGqZXtr3aXA==", + "dependencies": { + "@wojtekmaj/date-utils": "^1.0.3", + "clsx": "^1.2.1", + "get-user-locale": "^1.2.0", + "make-event-props": "^1.1.0", + "prop-types": "^15.6.0", + "react-calendar": "^4.0.0", + "react-clock": "^4.0.0", + "react-date-picker": "^9.1.0", + "react-fit": "^1.4.0", + "react-time-picker": "^5.1.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-datetime-picker?sponsor=1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -14297,6 +14340,28 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-time-picker": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-time-picker/-/react-time-picker-5.1.0.tgz", + "integrity": "sha512-NGpy5FM8WYdgCKXE/a85mhf0TunOk+N8ZG6rXBahD0mXeQZ/aBM+iVuKN4ell+QFH8/7F8dhT200ZKatBIphnA==", + "dependencies": { + "@wojtekmaj/date-utils": "^1.0.0", + "clsx": "^1.2.1", + "get-user-locale": "^1.2.0", + "make-event-props": "^1.1.0", + "prop-types": "^15.6.0", + "react-clock": "^4.0.0", + "react-fit": "^1.4.0", + "update-input-width": "^1.2.2" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-time-picker?sponsor=1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -19744,12 +19809,6 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, - "@types/luxon": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.0.2.tgz", - "integrity": "sha512-HM2OVWckUMmXbWYZufmWT2XMURGDZ6XbyNyQ+Lx+gCFGFqbZaIjsz7b+AGeGP/AuVYHBiuGY+wXfweP1RremnA==", - "dev": true - }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -19818,6 +19877,15 @@ "@types/react": "*" } }, + "@types/react-datetime-picker": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@types/react-datetime-picker/-/react-datetime-picker-3.4.1.tgz", + "integrity": "sha512-JHqB74+8Zq6cY0PTJ6Wi5Pm6qkNUmooyFfW5SiknSY2xJG1UG8+ljyWTZAvgHvj0XpqcWCHqqYUPiAVagnf9Sg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-dom": { "version": "18.0.4", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.4.tgz", @@ -21612,6 +21680,11 @@ "resolved": "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz", "integrity": "sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==" }, + "dayjs": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -25438,11 +25511,6 @@ "yallist": "^4.0.0" } }, - "luxon": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.0.4.tgz", - "integrity": "sha512-aV48rGUwP/Vydn8HT+5cdr26YYQiUZ42NM6ToMoaGKwYfWbfLeRkEu1wXWMHBZT6+KyLfcbbtVcoQFCbbPjKlw==" - }, "lz-string": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", @@ -26995,6 +27063,17 @@ "prop-types": "^15.6.0" } }, + "react-clock": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-clock/-/react-clock-4.0.0.tgz", + "integrity": "sha512-CBevN5B40TDUegSWzXk6bSwXhYzyerL9JGTme8GMAY0zO4FiEhVTGN1uzgC0rn/oSAMJw3M5wSf/OJpp9vcN2Q==", + "requires": { + "@wojtekmaj/date-utils": "^1.0.0", + "clsx": "^1.2.1", + "get-user-locale": "^1.4.0", + "prop-types": "^15.6.0" + } + }, "react-date-picker": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/react-date-picker/-/react-date-picker-9.1.0.tgz", @@ -27011,6 +27090,23 @@ "update-input-width": "^1.2.2" } }, + "react-datetime-picker": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/react-datetime-picker/-/react-datetime-picker-4.1.1.tgz", + "integrity": "sha512-e8ANKLcWFL4/TutvggqVfRiDigyelcVdLvWQzOP5cMJ6IxR08Qv2dOncjBWVkwU9vTsDemuHSAGIGqZXtr3aXA==", + "requires": { + "@wojtekmaj/date-utils": "^1.0.3", + "clsx": "^1.2.1", + "get-user-locale": "^1.2.0", + "make-event-props": "^1.1.0", + "prop-types": "^15.6.0", + "react-calendar": "^4.0.0", + "react-clock": "^4.0.0", + "react-date-picker": "^9.1.0", + "react-fit": "^1.4.0", + "react-time-picker": "^5.1.0" + } + }, "react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -27264,6 +27360,21 @@ "react-transition-group": "^4.3.0" } }, + "react-time-picker": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-time-picker/-/react-time-picker-5.1.0.tgz", + "integrity": "sha512-NGpy5FM8WYdgCKXE/a85mhf0TunOk+N8ZG6rXBahD0mXeQZ/aBM+iVuKN4ell+QFH8/7F8dhT200ZKatBIphnA==", + "requires": { + "@wojtekmaj/date-utils": "^1.0.0", + "clsx": "^1.2.1", + "get-user-locale": "^1.2.0", + "make-event-props": "^1.1.0", + "prop-types": "^15.6.0", + "react-clock": "^4.0.0", + "react-fit": "^1.4.0", + "update-input-width": "^1.2.2" + } + }, "react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3bc328d..62f174f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,10 +13,11 @@ "axios": "^0.27.2", "classnames": "^2.3.1", "daisyui": "^2.15.2", - "luxon": "^3.0.4", + "dayjs": "^1.11.7", "react": "^18.1.0", "react-big-calendar": "^1.5.0", "react-date-picker": "^9.1.0", + "react-datetime-picker": "^4.1.1", "react-dom": "^18.1.0", "react-hook-form": "^7.31.3", "react-modal": "^3.16.1", @@ -53,8 +54,8 @@ ] }, "devDependencies": { - "@types/luxon": "^3.0.2", "@types/react-big-calendar": "^0.38.2", + "@types/react-datetime-picker": "^3.4.1", "@types/react-modal": "^3.13.1", "autoprefixer": "^10.4.7", "postcss": "^8.4.14", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cf024cd..df90597 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -26,6 +26,11 @@ import SupervisorAvailabilities from './views/coordinator/SupervisorAvailabiliti import AvailabilitySchedule from './views/coordinator/AvailabilitySchedule' import GradeCard from './views/GradeCard' import Group from './views/coordinator/Group' +import dayjs from 'dayjs' +import WorkloadStatistics from './views/coordinator/WorkloadStatistics' + +require('dayjs/locale/pl') +dayjs.locale('pl') const queryClient = new QueryClient({ defaultOptions: { @@ -61,6 +66,10 @@ function App() { path="supervisors_availability/:id" element={} /> + } + /> }> } /> diff --git a/frontend/src/api/schedule.ts b/frontend/src/api/schedule.ts index e376d8d..090cedf 100644 --- a/frontend/src/api/schedule.ts +++ b/frontend/src/api/schedule.ts @@ -8,8 +8,9 @@ interface TermOfDefences { end_date: string title: string members_of_committee: { - members: { first_name: string; last_name: string }[] - } + first_name: string + last_name: string + }[] group: { name: string; students: Student[] } } @@ -191,3 +192,13 @@ export const setDateOfExaminationSchedule = ( export const generateTermsOfDefence = (scheduleId: number) => { return axiosInstance.post(`coordinator/enrollments/${scheduleId}/generate`) } + +export const geWorkloadStatistics = (scheduleId: number) => { + return axiosInstance.get<{ + workloads: { + assigned_to_committee: number + full_name: string + groups_assigned_to_his_committee: number + }[] + }>(`coordinator/examination_schedule/${scheduleId}/workloads/`) +} diff --git a/frontend/src/utils/dayjsLocalizer.js b/frontend/src/utils/dayjsLocalizer.js new file mode 100644 index 0000000..212c92e --- /dev/null +++ b/frontend/src/utils/dayjsLocalizer.js @@ -0,0 +1,409 @@ +import { DateLocalizer } from 'react-big-calendar' + +// import dayjs plugins +// Note that the timezone plugin is not imported here +// this plugin can be optionally loaded by the user +import isBetween from 'dayjs/plugin/isBetween' +import isSameOrAfter from 'dayjs/plugin/isSameOrAfter' +import isSameOrBefore from 'dayjs/plugin/isSameOrBefore' +import localeData from 'dayjs/plugin/localeData' +import localizedFormat from 'dayjs/plugin/localizedFormat' +import minMax from 'dayjs/plugin/minMax' +import utc from 'dayjs/plugin/utc' + +const weekRangeFormat = ({ start, end }, culture, local) => + local.format(start, 'MMMM DD', culture) + + ' – ' + + // updated to use this localizer 'eq()' method + local.format(end, local.eq(start, end, 'month') ? 'DD' : 'MMMM DD', culture) + +const dateRangeFormat = ({ start, end }, culture, local) => + local.format(start, 'L', culture) + ' – ' + local.format(end, 'L', culture) + +const timeRangeFormat = ({ start, end }, culture, local) => + local.format(start, 'LT', culture) + ' – ' + local.format(end, 'LT', culture) + +const timeRangeStartFormat = ({ start }, culture, local) => + local.format(start, 'LT', culture) + ' – ' + +const timeRangeEndFormat = ({ end }, culture, local) => + ' – ' + local.format(end, 'LT', culture) + +export const formats = { + dateFormat: 'DD', + dayFormat: 'DD ddd', + weekdayFormat: 'ddd', + + selectRangeFormat: timeRangeFormat, + eventTimeRangeFormat: timeRangeFormat, + eventTimeRangeStartFormat: timeRangeStartFormat, + eventTimeRangeEndFormat: timeRangeEndFormat, + + timeGutterFormat: 'LT', + + monthHeaderFormat: 'MMMM YYYY', + dayHeaderFormat: 'dddd MMM DD', + dayRangeHeaderFormat: weekRangeFormat, + agendaHeaderFormat: dateRangeFormat, + + agendaDateFormat: 'ddd MMM DD', + agendaTimeFormat: 'LT', + agendaTimeRangeFormat: timeRangeFormat, +} + +function fixUnit(unit) { + let datePart = unit ? unit.toLowerCase() : unit + if (datePart === 'FullYear') { + datePart = 'year' + } else if (!datePart) { + datePart = undefined + } + return datePart +} + +export default function (dayjsLib) { + // load dayjs plugins + dayjsLib.extend(isBetween) + dayjsLib.extend(isSameOrAfter) + dayjsLib.extend(isSameOrBefore) + dayjsLib.extend(localeData) + dayjsLib.extend(localizedFormat) + dayjsLib.extend(minMax) + dayjsLib.extend(utc) + + const locale = (dj, c) => (c ? dj.locale(c) : dj) + + // if the timezone plugin is loaded, + // then use the timezone aware version + const dayjs = dayjsLib.tz ? dayjsLib.tz : dayjsLib + + function getTimezoneOffset(date) { + // ensures this gets cast to timezone + return dayjs(date).toDate().getTimezoneOffset() + } + + function getDstOffset(start, end) { + // convert to dayjs, in case + const st = dayjs(start) + const ed = dayjs(end) + // if not using the dayjs timezone plugin + if (!dayjs.tz) { + return st.toDate().getTimezoneOffset() - ed.toDate().getTimezoneOffset() + } + /** + * If a default timezone has been applied, then + * use this to get the proper timezone offset, otherwise default + * the timezone to the browser local + */ + const tzName = st.tz().$x.$timezone ?? dayjsLib.tz.guess() + // invert offsets to be inline with moment.js + const startOffset = -dayjs.tz(+st, tzName).utcOffset() + const endOffset = -dayjs.tz(+ed, tzName).utcOffset() + return startOffset - endOffset + } + + function getDayStartDstOffset(start) { + const dayStart = dayjs(start).startOf('day') + return getDstOffset(dayStart, start) + } + + /*** BEGIN localized date arithmetic methods with dayjs ***/ + function defineComparators(a, b, unit) { + const datePart = fixUnit(unit) + const dtA = datePart ? dayjs(a).startOf(datePart) : dayjs(a) + const dtB = datePart ? dayjs(b).startOf(datePart) : dayjs(b) + return [dtA, dtB, datePart] + } + + function startOf(date = null, unit) { + const datePart = fixUnit(unit) + if (datePart) { + return dayjs(date).startOf(datePart).toDate() + } + return dayjs(date).toDate() + } + + function endOf(date = null, unit) { + const datePart = fixUnit(unit) + if (datePart) { + return dayjs(date).endOf(datePart).toDate() + } + return dayjs(date).toDate() + } + + // dayjs comparison operations *always* convert both sides to dayjs objects + // prior to running the comparisons + function eq(a, b, unit) { + const [dtA, dtB, datePart] = defineComparators(a, b, unit) + return dtA.isSame(dtB, datePart) + } + + function neq(a, b, unit) { + return !eq(a, b, unit) + } + + function gt(a, b, unit) { + const [dtA, dtB, datePart] = defineComparators(a, b, unit) + return dtA.isAfter(dtB, datePart) + } + + function lt(a, b, unit) { + const [dtA, dtB, datePart] = defineComparators(a, b, unit) + return dtA.isBefore(dtB, datePart) + } + + function gte(a, b, unit) { + const [dtA, dtB, datePart] = defineComparators(a, b, unit) + return dtA.isSameOrBefore(dtB, datePart) + } + + function lte(a, b, unit) { + const [dtA, dtB, datePart] = defineComparators(a, b, unit) + return dtA.isSameOrBefore(dtB, datePart) + } + + function inRange(day, min, max, unit = 'day') { + const datePart = fixUnit(unit) + const djDay = dayjs(day) + const djMin = dayjs(min) + const djMax = dayjs(max) + return djDay.isBetween(djMin, djMax, datePart, '[]') + } + + function min(dateA, dateB) { + const dtA = dayjs(dateA) + const dtB = dayjs(dateB) + const minDt = dayjsLib.min(dtA, dtB) + return minDt.toDate() + } + + function max(dateA, dateB) { + const dtA = dayjs(dateA) + const dtB = dayjs(dateB) + const maxDt = dayjsLib.max(dtA, dtB) + return maxDt.toDate() + } + + function merge(date, time) { + if (!date && !time) return null + + const tm = dayjs(time).format('HH:mm:ss') + const dt = dayjs(date).startOf('day').format('MM/DD/YYYY') + // We do it this way to avoid issues when timezone switching + return dayjsLib(`${dt} ${tm}`, 'MM/DD/YYYY HH:mm:ss').toDate() + } + + function add(date, adder, unit) { + const datePart = fixUnit(unit) + return dayjs(date).add(adder, datePart).toDate() + } + + function range(start, end, unit = 'day') { + const datePart = fixUnit(unit) + // because the add method will put these in tz, we have to start that way + let current = dayjs(start).toDate() + const days = [] + + while (lte(current, end)) { + days.push(current) + current = add(current, 1, datePart) + } + + return days + } + + function ceil(date, unit) { + const datePart = fixUnit(unit) + const floor = startOf(date, datePart) + + return eq(floor, date) ? floor : add(floor, 1, datePart) + } + + function diff(a, b, unit = 'day') { + const datePart = fixUnit(unit) + // don't use 'defineComparators' here, as we don't want to mutate the values + const dtA = dayjs(a) + const dtB = dayjs(b) + return dtB.diff(dtA, datePart) + } + + function minutes(date) { + const dt = dayjs(date) + return dt.minutes() + } + + function firstOfWeek(culture) { + const data = culture ? dayjsLib.localeData(culture) : dayjsLib.localeData() + return data ? data.firstDayOfWeek() : 0 + } + + function firstVisibleDay(date) { + return dayjs(date).startOf('month').startOf('week').toDate() + } + + function lastVisibleDay(date) { + return dayjs(date).endOf('month').endOf('week').toDate() + } + + function visibleDays(date) { + let current = firstVisibleDay(date) + const last = lastVisibleDay(date) + const days = [] + + while (lte(current, last)) { + days.push(current) + current = add(current, 1, 'd') + } + + return days + } + /*** END localized date arithmetic methods with dayjs ***/ + + /** + * Moved from TimeSlots.js, this method overrides the method of the same name + * in the localizer.js, using dayjs to construct the js Date + * @param {Date} dt - date to start with + * @param {Number} minutesFromMidnight + * @param {Number} offset + * @returns {Date} + */ + function getSlotDate(dt, minutesFromMidnight, offset) { + return dayjs(dt) + .startOf('day') + .minute(minutesFromMidnight + offset) + .toDate() + } + + // dayjs will automatically handle DST differences in it's calculations + function getTotalMin(start, end) { + return diff(start, end, 'minutes') + } + + function getMinutesFromMidnight(start) { + const dayStart = dayjs(start).startOf('day') + const day = dayjs(start) + return day.diff(dayStart, 'minutes') + getDayStartDstOffset(start) + } + + // These two are used by DateSlotMetrics + function continuesPrior(start, first) { + const djStart = dayjs(start) + const djFirst = dayjs(first) + return djStart.isBefore(djFirst, 'day') + } + + function continuesAfter(start, end, last) { + const djEnd = dayjs(end) + const djLast = dayjs(last) + return djEnd.isSameOrAfter(djLast, 'minutes') + } + + // These two are used by eventLevels + function sortEvents({ + evtA: { start: aStart, end: aEnd, allDay: aAllDay }, + evtB: { start: bStart, end: bEnd, allDay: bAllDay }, + }) { + const startSort = +startOf(aStart, 'day') - +startOf(bStart, 'day') + + const durA = diff(aStart, ceil(aEnd, 'day'), 'day') + + const durB = diff(bStart, ceil(bEnd, 'day'), 'day') + + return ( + startSort || // sort by start Day first + Math.max(durB, 1) - Math.max(durA, 1) || // events spanning multiple days go first + !!bAllDay - !!aAllDay || // then allDay single day events + +aStart - +bStart || // then sort by start time *don't need dayjs conversion here + +aEnd - +bEnd // then sort by end time *don't need dayjs conversion here either + ) + } + + function inEventRange({ + event: { start, end }, + range: { start: rangeStart, end: rangeEnd }, + }) { + const startOfDay = dayjs(start).startOf('day') + const eEnd = dayjs(end) + const rStart = dayjs(rangeStart) + const rEnd = dayjs(rangeEnd) + + const startsBeforeEnd = startOfDay.isSameOrBefore(rEnd, 'day') + // when the event is zero duration we need to handle a bit differently + const sameMin = !startOfDay.isSame(eEnd, 'minutes') + const endsAfterStart = sameMin + ? eEnd.isAfter(rStart, 'minutes') + : eEnd.isSameOrAfter(rStart, 'minutes') + + return startsBeforeEnd && endsAfterStart + } + + function isSameDate(date1, date2) { + const dt = dayjs(date1) + const dt2 = dayjs(date2) + return dt.isSame(dt2, 'day') + } + + /** + * This method, called once in the localizer constructor, is used by eventLevels + * 'eventSegments()' to assist in determining the 'span' of the event in the display, + * specifically when using a timezone that is greater than the browser native timezone. + * @returns number + */ + function browserTZOffset() { + /** + * Date.prototype.getTimezoneOffset horrifically flips the positive/negative from + * what you see in it's string, so we have to jump through some hoops to get a value + * we can actually compare. + */ + const dt = new Date() + const neg = /-/.test(dt.toString()) ? '-' : '' + const dtOffset = dt.getTimezoneOffset() + const comparator = Number(`${neg}${Math.abs(dtOffset)}`) + // dayjs correctly provides positive/negative offset, as expected + const mtOffset = dayjs().utcOffset() + return mtOffset > comparator ? 1 : 0 + } + + return new DateLocalizer({ + formats, + + firstOfWeek, + firstVisibleDay, + lastVisibleDay, + visibleDays, + + format(value, format, culture) { + return locale(dayjs(value), culture).format(format) + }, + + lt, + lte, + gt, + gte, + eq, + neq, + merge, + inRange, + startOf, + endOf, + range, + add, + diff, + ceil, + min, + max, + minutes, + + getSlotDate, + getTimezoneOffset, + getDstOffset, + getTotalMin, + getMinutesFromMidnight, + continuesPrior, + continuesAfter, + sortEvents, + inEventRange, + isSameDate, + browserTZOffset, + }) +} diff --git a/frontend/src/views/GradeCard.tsx b/frontend/src/views/GradeCard.tsx index 7f19e8f..dedde77 100644 --- a/frontend/src/views/GradeCard.tsx +++ b/frontend/src/views/GradeCard.tsx @@ -12,7 +12,7 @@ import styles from './GradeCard.module.css' // Import css modules stylesheet as const GradeCard = () => { const { register, handleSubmit, setValue } = useForm() const { id } = useParams<{ id: string }>() - const [supervisorId] = useLocalStorageState('supervisorId') + const [supervisorId] = useLocalStorageState('userId') useQuery( ['getGradesFirst'], diff --git a/frontend/src/views/Login.tsx b/frontend/src/views/Login.tsx index 499e6b0..38881fe 100644 --- a/frontend/src/views/Login.tsx +++ b/frontend/src/views/Login.tsx @@ -22,6 +22,8 @@ const Login = () => { const [userId, setUserId] = useLocalStorageState('userId', { defaultValue: -1, }) + const [studentId, setStudentId] = useLocalStorageState('studentId') + const [userType, setUserType] = useLocalStorageState('userType', { defaultValue: 'coordinator', }) @@ -66,19 +68,22 @@ const Login = () => { { onSuccess: (data) => { setLeaderOptions( - data?.data.project_supervisors.map(({ first_name, last_name, id }) => { - return { - value: id, - label: `${first_name} ${last_name}`, - } - }), + data?.data.project_supervisors.map( + ({ first_name, last_name, id }) => { + return { + value: id, + label: `${first_name} ${last_name}`, + } + }, + ), ) }, }, ) const onStudentChange = (v: any) => { - setUserId(v.value) + setStudentId(v.value) + // setUserId(v.value) setUserType('student') } @@ -131,7 +136,7 @@ const Login = () => { { {errors.limit_group?.type === 'pattern' && ( Limit grup musi być liczbą dodatnią )} - */} + ) diff --git a/frontend/src/views/coordinator/AvailabilitySchedule.tsx b/frontend/src/views/coordinator/AvailabilitySchedule.tsx index 34ed1a2..99593bc 100644 --- a/frontend/src/views/coordinator/AvailabilitySchedule.tsx +++ b/frontend/src/views/coordinator/AvailabilitySchedule.tsx @@ -1,5 +1,4 @@ -import { Calendar, luxonLocalizer, Views } from 'react-big-calendar' -import { DateTime, Settings } from 'luxon' +import { Calendar, Views } from 'react-big-calendar' import { useCallback, useState } from 'react' import { useMutation, useQuery } from 'react-query' import { useParams } from 'react-router-dom' @@ -9,14 +8,15 @@ import { generateTermsOfDefence, getAvailabilityForCoordinator, } from '../../api/schedule' +import dayjs from 'dayjs' +import dayjsLocalizer from '../../utils/dayjsLocalizer' + +const localizer = dayjsLocalizer(dayjs) const SupervisorSchedule = () => { - Settings.defaultZone = DateTime.local().zoneName - Settings.defaultLocale = 'pl' - const { id } = useParams<{ id: string }>() const [yearGroupId] = useLocalStorageState('yearGroupId') - const [supervisorId] = useLocalStorageState('supervisorId') + const [supervisorId] = useLocalStorageState('userId') const [events, setEvents] = useState< { id: number @@ -61,15 +61,15 @@ const SupervisorSchedule = () => { Generuj harmonogram diff --git a/frontend/src/views/coordinator/EditSchedule.tsx b/frontend/src/views/coordinator/EditSchedule.tsx index ac2d546..0d01fad 100644 --- a/frontend/src/views/coordinator/EditSchedule.tsx +++ b/frontend/src/views/coordinator/EditSchedule.tsx @@ -1,4 +1,4 @@ -import { DateTime } from 'luxon' +import dayjs from 'dayjs' import { useState } from 'react' import { Controller, NestedValue, useForm } from 'react-hook-form' import { useMutation, useQuery } from 'react-query' @@ -72,8 +72,8 @@ const EditSchedule = ({

Termin

- {DateTime.fromJSDate(eventData.start).toFormat('yyyy-LL-dd HH:mm:ss')}{' '} - - {DateTime.fromJSDate(eventData.end).toFormat('yyyy-LL-dd HH:mm:ss')} + {dayjs(eventData.start).format('YYYY-MM-DD HH:mm:ss')} -{' '} + {dayjs(eventData.end).format('YYYY-MM-DD HH:mm:ss')}

{eventData.resource?.members_of_committee?.length ? ( <> diff --git a/frontend/src/views/coordinator/Schedule.tsx b/frontend/src/views/coordinator/Schedule.tsx index b2505be..6797db0 100644 --- a/frontend/src/views/coordinator/Schedule.tsx +++ b/frontend/src/views/coordinator/Schedule.tsx @@ -1,5 +1,4 @@ -import { Calendar, luxonLocalizer, Views } from 'react-big-calendar' -import { DateTime, Settings } from 'luxon' +import { Calendar, Views } from 'react-big-calendar' import { useCallback, useEffect, useState } from 'react' import { useMutation, useQuery } from 'react-query' import { @@ -9,7 +8,7 @@ import { getTermsOfDefencesWithGroups, setDateOfExaminationSchedule, } from '../../api/schedule' -import { useParams } from 'react-router-dom' +import { Link, useParams } from 'react-router-dom' import Modal from 'react-modal' import { Controller, NestedValue, useForm } from 'react-hook-form' import EditSchedule from './EditSchedule' @@ -17,7 +16,9 @@ import Select from 'react-select' import { getLeaders } from '../../api/leaders' import useLocalStorageState from 'use-local-storage-state' import bigCalendarTranslations from '../../utils/bigCalendarTranslations' -import DatePicker from 'react-date-picker' +import DateTimePicker from 'react-datetime-picker' +import dayjs from 'dayjs' +import dayjsLocalizer from '../../utils/dayjsLocalizer' const customStyles = { content: { @@ -34,10 +35,9 @@ type SelectValue = { label: string } -const Schedule = () => { - Settings.defaultZone = DateTime.local().zoneName - Settings.defaultLocale = 'pl' +const localizer = dayjsLocalizer(dayjs) +const Schedule = () => { const { id } = useParams<{ id: string }>() const [yearGroupId] = useLocalStorageState('yearGroupId') const [events, setEvents] = useState< @@ -184,10 +184,10 @@ const Schedule = () => { } else { // await mutateCreateEvent({ // start_date: DateTime.fromJSDate(event.start).toFormat( - // 'yyyy-LL-dd HH:mm:ss', + // 'YYYY-MM-DD HH:mm:ss', // ), // end_date: DateTime.fromJSDate(event.end).toFormat( - // 'yyyy-LL-dd HH:mm:ss', + // 'YYYY-MM-DD HH:mm:ss', // ), // scheduleId: Number(id), // }) @@ -217,12 +217,14 @@ const Schedule = () => { const from = data.from.split(':') const to = data.to.split(':') await mutateCreateEvent({ - start_date: DateTime.fromJSDate(selectedDate.start) - .set({ hour: from[0], minute: from[1] }) - .toFormat('yyyy-LL-dd HH:mm:ss'), - end_date: DateTime.fromJSDate(selectedDate.start) - .set({ hour: to[0], minute: to[1] }) - .toFormat('yyyy-LL-dd HH:mm:ss'), + start_date: dayjs(selectedDate.start) + .set('hour', from[0]) + .set('minute', from[1]) + .format('YYYY-MM-DD HH:mm:ss'), + end_date: dayjs(selectedDate.start) + .set('hour', to[0]) + .set('minute', to[1]) + .format('YYYY-MM-DD HH:mm:ss'), scheduleId: Number(id), project_supervisors: data?.project_supervisors, }) @@ -278,16 +280,20 @@ const Schedule = () => {
Start zapisów dla studentów: - -
- - +
+ + Statystyki + + +
{ onView={onView} view={view} eventPropGetter={eventGetter} - min={DateTime.fromObject({ hour: 8, minute: 0 }).toJSDate()} - max={DateTime.fromObject({ hour: 16, minute: 0 }).toJSDate()} + min={dayjs().set('hour', 8).set('minute', 0).toDate()} + max={dayjs().set('hour', 16).set('minute', 0).toDate()} messages={bigCalendarTranslations} /> diff --git a/frontend/src/views/coordinator/Schedules.tsx b/frontend/src/views/coordinator/Schedules.tsx index 16cb8d4..459f02c 100644 --- a/frontend/src/views/coordinator/Schedules.tsx +++ b/frontend/src/views/coordinator/Schedules.tsx @@ -3,7 +3,7 @@ import { createSchedule, getSchedules } from '../../api/schedule' import { useForm } from 'react-hook-form' import { Link } from 'react-router-dom' import useLocalStorageState from 'use-local-storage-state' -import DatePicker from 'react-date-picker' +import DateTimePicker from 'react-datetime-picker' import { useState } from 'react' const Schedules = () => { @@ -60,20 +60,24 @@ const Schedules = () => { Od - -
diff --git a/frontend/src/views/coordinator/WorkloadStatistics.tsx b/frontend/src/views/coordinator/WorkloadStatistics.tsx new file mode 100644 index 0000000..05a472b --- /dev/null +++ b/frontend/src/views/coordinator/WorkloadStatistics.tsx @@ -0,0 +1,35 @@ +import { useQuery } from 'react-query' +import { useParams } from 'react-router-dom' +import { geWorkloadStatistics } from '../../api/schedule' + +const WorkloadStatistics = () => { + const { id } = useParams<{ id: string }>() + + const { data: workloads } = useQuery(['workload'], () => + geWorkloadStatistics(Number(id)), + ) + return ( +
+ + + + + + + + + {workloads?.data?.workloads.map( + ({ full_name, assigned_to_committee }) => ( + + + + + ), + )} + +
Imię i nazwiskoIlość przypisanych komisji
{full_name}{assigned_to_committee}
+
+ ) +} + +export default WorkloadStatistics diff --git a/frontend/src/views/student/ScheduleAddGroup.tsx b/frontend/src/views/student/ScheduleAddGroup.tsx index 5c5278a..ec65e79 100644 --- a/frontend/src/views/student/ScheduleAddGroup.tsx +++ b/frontend/src/views/student/ScheduleAddGroup.tsx @@ -1,4 +1,4 @@ -import { DateTime } from 'luxon' +import dayjs from 'dayjs' import React, { useState } from 'react' import { useForm } from 'react-hook-form' import { useMutation } from 'react-query' @@ -51,9 +51,8 @@ const ScheduleAddGroup = ({

- Termin{' '} - {DateTime.fromJSDate(eventData.start).toFormat('yyyy-LL-dd HH:mm:ss')}{' '} - - {DateTime.fromJSDate(eventData.end).toFormat('yyyy-LL-dd HH:mm:ss')} + Termin {dayjs(eventData.start).format('YYYY-MM-DD HH:mm:ss')} -{' '} + {dayjs(eventData.end).format('YYYY-MM-DD HH:mm:ss')}

{eventData.resource?.members_of_committee?.length > 0 && ( <> diff --git a/frontend/src/views/student/StudentSchedule.tsx b/frontend/src/views/student/StudentSchedule.tsx index 8c58d6a..af66ed0 100644 --- a/frontend/src/views/student/StudentSchedule.tsx +++ b/frontend/src/views/student/StudentSchedule.tsx @@ -1,5 +1,4 @@ -import { Calendar, luxonLocalizer, Views } from 'react-big-calendar' -import { DateTime, Settings } from 'luxon' +import { Calendar, Views } from 'react-big-calendar' import { useCallback, useState } from 'react' import { useQuery } from 'react-query' import { getStudentsTermsOfDefences } from '../../api/schedule' @@ -9,6 +8,8 @@ import { useForm } from 'react-hook-form' import ScheduleAddGroup from './ScheduleAddGroup' import bigCalendarTranslations from '../../utils/bigCalendarTranslations' import useLocalStorageState from 'use-local-storage-state' +import dayjs from 'dayjs' +import dayjsLocalizer from '../../utils/dayjsLocalizer' const customStyles = { content: { @@ -20,11 +21,9 @@ const customStyles = { transform: 'translate(-50%, -50%)', }, } +const localizer = dayjsLocalizer(dayjs) const StudentSchedule = () => { - Settings.defaultZone = DateTime.local().zoneName - Settings.defaultLocale = 'pl' - const [studentId] = useLocalStorageState('studentId') const { id } = useParams<{ id: string }>() const [events, setEvents] = useState< @@ -131,7 +130,7 @@ const StudentSchedule = () => { Wybierz i zatwierdź termin obrony dla swojej grupy { onView={onView} view={view} eventPropGetter={eventGetter} - min={DateTime.fromObject({ hour: 8, minute: 0 }).toJSDate()} - max={DateTime.fromObject({ hour: 16, minute: 0 }).toJSDate()} + min={dayjs().set('hour', 8).set('minute', 0).toDate()} + max={dayjs().set('hour', 16).set('minute', 0).toDate()} messages={bigCalendarTranslations} /> diff --git a/frontend/src/views/supervisor/SupervisorSchedule.tsx b/frontend/src/views/supervisor/SupervisorSchedule.tsx index c51c213..924baa1 100644 --- a/frontend/src/views/supervisor/SupervisorSchedule.tsx +++ b/frontend/src/views/supervisor/SupervisorSchedule.tsx @@ -1,5 +1,4 @@ -import { Calendar, luxonLocalizer, Views } from 'react-big-calendar' -import { DateTime, Settings } from 'luxon' +import { Calendar, Views } from 'react-big-calendar' import { useCallback, useState } from 'react' import { useMutation, useQuery } from 'react-query' import { @@ -13,6 +12,8 @@ import { useForm } from 'react-hook-form' import EditSchedule from '../coordinator/EditSchedule' import useLocalStorageState from 'use-local-storage-state' import bigCalendarTranslations from '../../utils/bigCalendarTranslations' +import dayjs from 'dayjs' +import dayjsLocalizer from '../../utils/dayjsLocalizer' const customStyles = { content: { @@ -28,11 +29,9 @@ type SelectValue = { value: string | number label: string } +const localizer = dayjsLocalizer(dayjs) const SupervisorSchedule = () => { - Settings.defaultZone = DateTime.local().zoneName - Settings.defaultLocale = 'pl' - const { id } = useParams<{ id: string }>() const [yearGroupId] = useLocalStorageState('yearGroupId') const [supervisorId] = useLocalStorageState('userId') @@ -155,12 +154,14 @@ const SupervisorSchedule = () => { const from = data.from.split(':') const to = data.to.split(':') await mutateAddAvailability({ - start_date: DateTime.fromJSDate(selectedDate.start) - .set({ hour: from[0], minute: from[1] }) - .toFormat('yyyy-LL-dd HH:mm:ss'), - end_date: DateTime.fromJSDate(selectedDate.start) - .set({ hour: to[0], minute: to[1] }) - .toFormat('yyyy-LL-dd HH:mm:ss'), + start_date: dayjs(selectedDate.start) + .set('hour', from[0]) + .set('minute', from[1]) + .format('YYYY-MM-DD HH:mm:ss'), + end_date: dayjs(selectedDate.start) + .set('hour', to[0]) + .set('minute', to[1]) + .format('YYYY-MM-DD HH:mm:ss'), scheduleId: Number(id), project_supervisor_id: Number(supervisorId), }) @@ -189,7 +190,7 @@ const SupervisorSchedule = () => { return (
{ onView={onView} view={view} eventPropGetter={eventGetter} - min={DateTime.fromObject({ hour: 8, minute: 0 }).toJSDate()} - max={DateTime.fromObject({ hour: 16, minute: 0 }).toJSDate()} + min={dayjs().set('hour', 8).set('minute', 0).toDate()} + max={dayjs().set('hour', 16).set('minute', 0).toDate()} messages={bigCalendarTranslations} />