Room reservations planning

This commit is contained in:
imperosol 2025-06-27 18:47:47 +02:00
parent 79fc6e3859
commit de7caea9a5
11 changed files with 395 additions and 120 deletions

View File

@ -81,9 +81,8 @@
} }
#links_content { #links_content {
overflow: auto;
box-shadow: $shadow-color 1px 1px 1px; box-shadow: $shadow-color 1px 1px 1px;
height: 20em; padding: .5rem;
h4 { h4 {
margin-left: 5px; margin-left: 5px;

View File

@ -1,13 +1,11 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from "com/macros.jinja" import news_moderation_alert %} {% from "com/macros.jinja" import news_moderation_alert %}
{% block title %} {% block title %}AE UTBM{% endblock %}
{% trans %}News{% endtrans %}
{% endblock %}
{% block additional_css %} {% block additional_css %}
<link rel="stylesheet" href="{{ static('com/css/news-list.scss') }}"> <link rel="stylesheet" href="{{ static('com/css/news-list.scss') }}">
<link rel="stylesheet" href="{{ static('com/components/ics-calendar.scss') }}"> <link rel="stylesheet" href="{{ static('core/components/calendar.scss') }}">
{# Atom feed discovery, not really css but also goes there #} {# Atom feed discovery, not really css but also goes there #}
<link rel="alternate" type="application/rss+xml" title="{% trans %}News feed{% endtrans %}" href="{{ url("com:news_feed") }}"> <link rel="alternate" type="application/rss+xml" title="{% trans %}News feed{% endtrans %}" href="{{ url("com:news_feed") }}">
@ -213,6 +211,10 @@
<i class="fa-solid fa-magnifying-glass fa-xl"></i> <i class="fa-solid fa-magnifying-glass fa-xl"></i>
<a href="{{ url("matmat:search_clear") }}">{% trans %}Matmatronch{% endtrans %}</a> <a href="{{ url("matmat:search_clear") }}">{% trans %}Matmatronch{% endtrans %}</a>
</li> </li>
<li>
<i class="fa-solid fa-thumbtack fa-xl"></i>
<a href="{{ url("reservation:main") }}">{% trans %}Room reservation{% endtrans %}</a>
</li>
<li> <li>
<i class="fa-solid fa-check-to-slot fa-xl"></i> <i class="fa-solid fa-check-to-slot fa-xl"></i>
<a href="{{ url("election:list") }}">{% trans %}Elections{% endtrans %}</a> <a href="{{ url("election:list") }}">{% trans %}Elections{% endtrans %}</a>

View File

@ -16,14 +16,74 @@
--event-details-padding: 20px; --event-details-padding: 20px;
--event-details-border: 1px solid #EEEEEE; --event-details-border: 1px solid #EEEEEE;
--event-details-border-radius: 4px; --event-details-border-radius: 4px;
--event-details-box-shadow: 0px 6px 20px 4px rgb(0 0 0 / 16%); --event-details-box-shadow: 0 6px 20px 4px rgb(0 0 0 / 16%);
--event-details-max-width: 600px; --event-details-max-width: 600px;
} }
ics-calendar { ics-calendar,
room-scheduler {
border: none; border: none;
box-shadow: none; box-shadow: none;
a.fc-col-header-cell-cushion,
a.fc-col-header-cell-cushion:hover {
color: black;
}
a.fc-daygrid-day-number,
a.fc-daygrid-day-number:hover {
color: rgb(34, 34, 34);
}
td {
overflow: visible; // Show events on multiple days
}
td, th {
text-align: unset;
}
//Reset from style.scss
table {
box-shadow: none;
border-radius: 0;
-moz-border-radius: 0;
margin: 0;
}
// Reset from style.scss
thead {
background-color: white;
color: black;
}
// Reset from style.scss
tbody > tr {
&:nth-child(even):not(.highlight) {
background: white;
}
}
.fc .fc-toolbar.fc-footer-toolbar {
margin-bottom: 0.5em;
}
button.text-copy,
button.text-copy:focus,
button.text-copy:hover {
background-color: #67AE6E !important;
transition: 500ms ease-in;
}
button.text-copied,
button.text-copied:focus,
button.text-copied:hover {
transition: 500ms ease-out;
}
}
ics-calendar {
#event-details { #event-details {
z-index: 10; z-index: 10;
max-width: 1151px; max-width: 1151px;
@ -60,68 +120,47 @@ ics-calendar {
align-items: start; align-items: start;
flex-direction: row; flex-direction: row;
background-color: var(--event-details-background-color); background-color: var(--event-details-background-color);
margin-top: 0px; margin-top: 0;
margin-bottom: 4px; margin-bottom: 4px;
} }
} }
}
a.fc-col-header-cell-cushion, // Reset from style.scss
a.fc-col-header-cell-cushion:hover { thead {
color: black;
}
a.fc-daygrid-day-number,
a.fc-daygrid-day-number:hover {
color: rgb(34, 34, 34);
}
td {
overflow: visible; // Show events on multiple days
}
//Reset from style.scss
table {
box-shadow: none;
border-radius: 0px;
-moz-border-radius: 0px;
margin: 0px;
}
// Reset from style.scss
thead {
background-color: white; background-color: white;
color: black; color: black;
} }
// Reset from style.scss // Reset from style.scss
tbody>tr { tbody > tr {
&:nth-child(even):not(.highlight) { &:nth-child(even):not(.highlight) {
background: white; background: white;
} }
} }
.fc .fc-toolbar.fc-footer-toolbar { .fc .fc-toolbar.fc-footer-toolbar {
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
button.text-copy, button.text-copy,
button.text-copy:focus, button.text-copy:focus,
button.text-copy:hover { button.text-copy:hover {
background-color: #67AE6E !important; background-color: #67AE6E !important;
transition: 500ms ease-in; transition: 500ms ease-in;
} }
button.text-copied, button.text-copied,
button.text-copied:focus, button.text-copied:focus,
button.text-copied:hover { button.text-copied:hover {
transition: 500ms ease-out; transition: 500ms ease-out;
} }
.fc .fc-getCalendarLink-button { .fc .fc-getCalendarLink-button {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.fc .fc-helpButton-button { .fc .fc-helpButton-button {
border-radius: 70%; border-radius: 70%;
padding-left: 0.5rem; padding-left: 0.5rem;
padding-right: 0.5rem; padding-right: 0.5rem;
@ -130,12 +169,11 @@ ics-calendar {
width: 30px; width: 30px;
height: 30px; height: 30px;
font-size: 11px; font-size: 11px;
} }
.fc .fc-helpButton-button:hover { .fc .fc-helpButton-button:hover {
background-color: rgba(20, 20, 20, 0.6); background-color: rgba(20, 20, 20, 0.6);
}
} }
.tooltip.calendar-copy-tooltip { .tooltip.calendar-copy-tooltip {

View File

@ -1,4 +1,4 @@
type ErrorMessage = string; declare type ErrorMessage = string;
export interface InitialFormData { export interface InitialFormData {
/* Used to refill the form when the backend raises an error */ /* Used to refill the form when the backend raises an error */

158
package-lock.json generated
View File

@ -13,10 +13,14 @@
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0", "@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.6.0",
"@fullcalendar/core": "^6.1.15", "@fullcalendar/core": "^6.1.17",
"@fullcalendar/daygrid": "^6.1.15", "@fullcalendar/daygrid": "^6.1.17",
"@fullcalendar/icalendar": "^6.1.15", "@fullcalendar/icalendar": "^6.1.17",
"@fullcalendar/list": "^6.1.15", "@fullcalendar/interaction": "^6.1.17",
"@fullcalendar/list": "^6.1.17",
"@fullcalendar/resource": "^6.1.17",
"@fullcalendar/resource-timeline": "^6.1.17",
"@hey-api/client-fetch": "^0.8.2",
"@sentry/browser": "^9.29.0", "@sentry/browser": "^9.29.0",
"@zip.js/zip.js": "^2.7.52", "@zip.js/zip.js": "^2.7.52",
"3d-force-graph": "^1.73.4", "3d-force-graph": "^1.73.4",
@ -2224,6 +2228,15 @@
"ical.js": "^1.4.0" "ical.js": "^1.4.0"
} }
}, },
"node_modules/@fullcalendar/interaction": {
"version": "6.1.17",
"resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.17.tgz",
"integrity": "sha512-AudvQvgmJP2FU89wpSulUUjeWv24SuyCx8FzH2WIPVaYg+vDGGYarI7K6PcM3TH7B/CyaBjm5Rqw9lXgnwt5YA==",
"license": "MIT",
"peerDependencies": {
"@fullcalendar/core": "~6.1.17"
}
},
"node_modules/@fullcalendar/list": { "node_modules/@fullcalendar/list": {
"version": "6.1.17", "version": "6.1.17",
"resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-6.1.17.tgz", "resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-6.1.17.tgz",
@ -2233,6 +2246,77 @@
"@fullcalendar/core": "~6.1.17" "@fullcalendar/core": "~6.1.17"
} }
}, },
"node_modules/@fullcalendar/premium-common": {
"version": "6.1.17",
"resolved": "https://registry.npmjs.org/@fullcalendar/premium-common/-/premium-common-6.1.17.tgz",
"integrity": "sha512-zoN7fMwGMcP6Xu+2YudRAGfdwD2J+V+A/xAieXgYDSZT+5ekCsjZiwb2rmvthjt+HVnuZcqs6sGp7rnJ8Ie/mA==",
"license": "SEE LICENSE IN LICENSE.md",
"peerDependencies": {
"@fullcalendar/core": "~6.1.17"
}
},
"node_modules/@fullcalendar/resource": {
"version": "6.1.17",
"resolved": "https://registry.npmjs.org/@fullcalendar/resource/-/resource-6.1.17.tgz",
"integrity": "sha512-hWnbOWlroIN5Wt4NJmHAJh/F7ge2cV6S0PdGSmLFoZJZJA0hJX9GeYRzyz4MlUoj7f4dGzBlesy2RdC+t5FEMw==",
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"@fullcalendar/premium-common": "~6.1.17"
},
"peerDependencies": {
"@fullcalendar/core": "~6.1.17"
}
},
"node_modules/@fullcalendar/resource-timeline": {
"version": "6.1.17",
"resolved": "https://registry.npmjs.org/@fullcalendar/resource-timeline/-/resource-timeline-6.1.17.tgz",
"integrity": "sha512-QMrtc1mLs4c6DtlBNmWICef8Lr4CmzE47uWS/rcJBd9K2kBzvusTp7AQQ1qn3RX5UnjNHqT8pkKO/wE4yspJQw==",
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"@fullcalendar/premium-common": "~6.1.17",
"@fullcalendar/scrollgrid": "~6.1.17",
"@fullcalendar/timeline": "~6.1.17"
},
"peerDependencies": {
"@fullcalendar/core": "~6.1.17",
"@fullcalendar/resource": "~6.1.17"
}
},
"node_modules/@fullcalendar/scrollgrid": {
"version": "6.1.17",
"resolved": "https://registry.npmjs.org/@fullcalendar/scrollgrid/-/scrollgrid-6.1.17.tgz",
"integrity": "sha512-lzphEKwxWMS4xQVEuimzZjKFLijlSn49ExvzkYZls0VLDwOa3BYHcRlDJBjQ0LP6kauz9aatg3MfRIde/LAazA==",
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"@fullcalendar/premium-common": "~6.1.17"
},
"peerDependencies": {
"@fullcalendar/core": "~6.1.17"
}
},
"node_modules/@fullcalendar/timeline": {
"version": "6.1.17",
"resolved": "https://registry.npmjs.org/@fullcalendar/timeline/-/timeline-6.1.17.tgz",
"integrity": "sha512-UhL2OOph/S0cEKs3lzbXjS2gTxmQwaNug2XFjdljvO/ERj10v7OBXj/zvJrPyhjvWR/CSgjNgBaUpngkCu4JtQ==",
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"@fullcalendar/premium-common": "~6.1.17",
"@fullcalendar/scrollgrid": "~6.1.17"
},
"peerDependencies": {
"@fullcalendar/core": "~6.1.17"
}
},
"node_modules/@hey-api/client-fetch": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/@hey-api/client-fetch/-/client-fetch-0.8.4.tgz",
"integrity": "sha512-SWtUjVEFIUdiJGR2NiuF0njsSrSdTe7WHWkp3BLH3DEl2bRhiflOnBo29NSDdrY90hjtTQiTQkBxUgGOF29Xzg==",
"deprecated": "Starting with v0.73.0, this package is bundled directly inside @hey-api/openapi-ts.",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/hey-api"
}
},
"node_modules/@hey-api/json-schema-ref-parser": { "node_modules/@hey-api/json-schema-ref-parser": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.0.6.tgz", "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.0.6.tgz",
@ -2726,75 +2810,75 @@
] ]
}, },
"node_modules/@sentry-internal/browser-utils": { "node_modules/@sentry-internal/browser-utils": {
"version": "9.29.0", "version": "9.33.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.29.0.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.33.0.tgz",
"integrity": "sha512-Wp6UJCDVV2KVK+TG8GwdLZyDy4GtUYDmVhGMpHKPS3G/Qgpf36cY/XHwChwaHZ5P9Bk1sjS9Ok698J59S8L2nw==", "integrity": "sha512-DT9J0jIamavygIvW6rapgFb4L+7VoATPfEaV0UnXfGNXpSq18x7+vj1CyGMc//GBqqgb9SCHxJHOSkfuDYX7ZA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry/core": "9.29.0" "@sentry/core": "9.33.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry-internal/feedback": { "node_modules/@sentry-internal/feedback": {
"version": "9.29.0", "version": "9.33.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.29.0.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.33.0.tgz",
"integrity": "sha512-ADvetGrtr+RfYcQKrQxah4fHs/xDJ/VjbStVMSuaNllzwWPYNkWIGFE6YjQ7wZszj0DQIu5/H+B6lZKsFYk4xw==", "integrity": "sha512-NQ3Q3d1xvtagI2cYZnI6C1i6hmMkUxIXUMjfO5JFTYpWGNIkzhIaoaY0HFqbiZ94FWwWdfodlQlj6r8Y+M0bnw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry/core": "9.29.0" "@sentry/core": "9.33.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry-internal/replay": { "node_modules/@sentry-internal/replay": {
"version": "9.29.0", "version": "9.33.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.29.0.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.33.0.tgz",
"integrity": "sha512-we/1JPRje8sNowQCyogOV1OYWuDOP/3XmDi48XoFG2HB0XMl2HfL5LI8AvgAvC/5nrqVAAo4ktbjoVLm1fb7rg==", "integrity": "sha512-xDFrN19hDkP6+yS4ARYBruI0RinGYD8FPm7JC0BaIMP5yNWAJ80LTT0Jq9Dh1hQfDwUX34dpHy/9Aa7qv+2bRQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry-internal/browser-utils": "9.29.0", "@sentry-internal/browser-utils": "9.33.0",
"@sentry/core": "9.29.0" "@sentry/core": "9.33.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry-internal/replay-canvas": { "node_modules/@sentry-internal/replay-canvas": {
"version": "9.29.0", "version": "9.33.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.29.0.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.33.0.tgz",
"integrity": "sha512-TrQYhSAVPhyenvu0fNkon7BznFibu1mzS5bCudxhgOWajZluUVrXcbp8Q3WZ3R+AogrcgA3Vy6aumP/+fMKdwg==", "integrity": "sha512-lFO5DYJ32K/mui5Ck7PbqcD7wzRxTyRKiy49gCGAp7x/mhLg5utf5vWPtegiUoCiiMB22rj+n2z0geZwiGKH4A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry-internal/replay": "9.29.0", "@sentry-internal/replay": "9.33.0",
"@sentry/core": "9.29.0" "@sentry/core": "9.33.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry/browser": { "node_modules/@sentry/browser": {
"version": "9.29.0", "version": "9.33.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.29.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.33.0.tgz",
"integrity": "sha512-+GFX/yb+rh6V1fSgTYM6ttAgledl2aUR3T3Rg86HNuegbdX8ym6lOtUOIZ0j9jPK015HR47KIPyIZVZZJ7Rj9g==", "integrity": "sha512-emlZlpE62lcpxMEzvrQzecnh0WeS36XLQlFLEUhGaYVOw7TBl5JPIoSB4mxPrzIn4GpW++3JrtKRpDAHQn/c4Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry-internal/browser-utils": "9.29.0", "@sentry-internal/browser-utils": "9.33.0",
"@sentry-internal/feedback": "9.29.0", "@sentry-internal/feedback": "9.33.0",
"@sentry-internal/replay": "9.29.0", "@sentry-internal/replay": "9.33.0",
"@sentry-internal/replay-canvas": "9.29.0", "@sentry-internal/replay-canvas": "9.33.0",
"@sentry/core": "9.29.0" "@sentry/core": "9.33.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry/core": { "node_modules/@sentry/core": {
"version": "9.29.0", "version": "9.33.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.29.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.33.0.tgz",
"integrity": "sha512-wDyNe45PM+RCGtUn1tK7LzJ08ksv8i8KRUHrst7lsinEfRm83YH+wbWrPmwkVNEngUZvYkHwGLbNXM7xgFUuDQ==", "integrity": "sha512-0mtJAU+x10+q5aV/txyeuPjJ0TmObcD701R0tY0s71yJJOltqqMrmgNpqyuMI/VOASuzTZesiMYdbG6xb3zeSw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@ -5803,9 +5887,9 @@
} }
}, },
"node_modules/vite-plugin-static-copy": { "node_modules/vite-plugin-static-copy": {
"version": "3.0.2", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.0.2.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.0.tgz",
"integrity": "sha512-/seLvhUg44s1oU9RhjTZZy/0NPbfNctozdysKcvPovxxXZdI5l19mGq6Ri3IaTf1Dy/qChS4BSR7ayxeu8o9aQ==", "integrity": "sha512-ONFBaYoN1qIiCxMCfeHI96lqLza7ujx/QClIXp4kEULUbyH2qLgYoaL8JHhk3FWjSB4TpzoaN3iMCyCFldyXzw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -5819,7 +5903,7 @@
"node": "^18.0.0 || >=20.0.0" "node": "^18.0.0 || >=20.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"vite": "^5.0.0 || ^6.0.0" "vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
} }
}, },
"node_modules/vite-plugin-static-copy/node_modules/chokidar": { "node_modules/vite-plugin-static-copy/node_modules/chokidar": {

View File

@ -21,7 +21,8 @@
"#core:*": "./core/static/bundled/*", "#core:*": "./core/static/bundled/*",
"#pedagogy:*": "./pedagogy/static/bundled/*", "#pedagogy:*": "./pedagogy/static/bundled/*",
"#counter:*": "./counter/static/bundled/*", "#counter:*": "./counter/static/bundled/*",
"#com:*": "./com/static/bundled/*" "#com:*": "./com/static/bundled/*",
"#reservation:*": "./reservation/static/bundled/*"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
@ -43,10 +44,14 @@
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0", "@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.6.0",
"@fullcalendar/core": "^6.1.15", "@fullcalendar/core": "^6.1.17",
"@fullcalendar/daygrid": "^6.1.15", "@fullcalendar/daygrid": "^6.1.17",
"@fullcalendar/icalendar": "^6.1.15", "@fullcalendar/icalendar": "^6.1.17",
"@fullcalendar/list": "^6.1.15", "@fullcalendar/interaction": "^6.1.17",
"@fullcalendar/list": "^6.1.17",
"@fullcalendar/resource": "^6.1.17",
"@fullcalendar/resource-timeline": "^6.1.17",
"@hey-api/client-fetch": "^0.8.2",
"@sentry/browser": "^9.29.0", "@sentry/browser": "^9.29.0",
"@zip.js/zip.js": "^2.7.52", "@zip.js/zip.js": "^2.7.52",
"3d-force-graph": "^1.73.4", "3d-force-graph": "^1.73.4",

View File

@ -0,0 +1,120 @@
import { inheritHtmlElement, registerComponent } from "#core:utils/web-components";
import {
Calendar,
type EventDropArg,
type EventSourceFuncArg,
} from "@fullcalendar/core";
import enLocale from "@fullcalendar/core/locales/en-gb";
import frLocale from "@fullcalendar/core/locales/fr";
import {
type ReservationslotFetchSlotsData,
type SlotSchema,
reservableroomFetchRooms,
reservationslotFetchSlots,
reservationslotUpdateSlot,
} from "#openapi";
import { paginated } from "#core:utils/api";
import interactionPlugin from "@fullcalendar/interaction";
import resourceTimelinePlugin from "@fullcalendar/resource-timeline";
@registerComponent("room-scheduler")
export class RoomScheduler extends inheritHtmlElement("div") {
static observedAttributes = ["locale", "can_edit_slot", "can_create_slot"];
private scheduler: Calendar;
private locale = "en";
private canEditSlot = false;
private canBookSlot = false;
attributeChangedCallback(name: string, _oldValue?: string, newValue?: string) {
if (name === "locale") {
this.locale = newValue;
}
if (name === "can_edit_slot") {
this.canEditSlot = newValue.toLowerCase() === "true";
}
if (name === "can_create_slot") {
this.canBookSlot = newValue.toLowerCase() === "true";
}
}
/**
* Fetch the events displayed in the timeline.
* cf https://fullcalendar.io/docs/events-function
*/
async fetchEvents(fetchInfo: EventSourceFuncArg) {
const res: SlotSchema[] = await paginated(reservationslotFetchSlots, {
query: { after: fetchInfo.startStr, before: fetchInfo.endStr },
} as ReservationslotFetchSlotsData);
return res.map((i) =>
Object.assign(i, {
title: `${i.author.first_name} ${i.author.last_name}`,
resourceId: i.room,
editable: new Date(i.start) > new Date(),
}),
);
}
/**
* Fetch the resources which events are associated with.
* cf https://fullcalendar.io/docs/resources-function
*/
async fetchResources() {
const res = await reservableroomFetchRooms();
return res.data.map((i) => Object.assign(i, { title: i.name, group: i.location }));
}
/**
* Send a request to the API to change
* the start and the duration of a reservation slot
*/
async changeReservation(args: EventDropArg) {
const duration = new Date(args.event.end.getTime() - args.event.start.getTime());
const response = await reservationslotUpdateSlot({
// biome-ignore lint/style/useNamingConvention: api is snake_case
path: { slot_id: Number.parseInt(args.event.id) },
query: {
start: args.event.startStr,
duration: `PT${duration.getUTCHours()}H${duration.getUTCMinutes()}M${duration.getUTCSeconds()}S`,
},
});
if (response.response.ok) {
this.scheduler.refetchEvents();
}
}
connectedCallback() {
super.connectedCallback();
this.scheduler = new Calendar(this.node, {
schedulerLicenseKey: "GPL-My-Project-Is-Open-Source",
initialView: "resourceTimelineDay",
headerToolbar: {
left: "prev,next today",
center: "title",
right: "resourceTimelineDay,resourceTimelineWeek",
},
plugins: [resourceTimelinePlugin, interactionPlugin],
locales: [frLocale, enLocale],
height: "auto",
locale: this.locale,
resourceGroupField: "group",
resourceAreaHeaderContent: gettext("Rooms"),
editable: this.canEditSlot,
snapDuration: "00:15",
eventConstraint: { start: new Date() }, // forbid edition of past events
eventOverlap: false,
eventResourceEditable: false,
refetchResourcesOnNavigate: true,
resourceAreaWidth: "20%",
resources: this.fetchResources,
events: this.fetchEvents,
selectOverlap: false,
selectable: this.canBookSlot,
selectConstraint: { start: new Date() },
nowIndicator: true,
eventDrop: this.changeReservation,
});
this.scheduler.render();
}
}

View File

@ -0,0 +1,21 @@
{% extends "core/base.jinja" %}
{% block additional_js %}
<script type="module" src="{{ static("bundled/reservation/components/room-scheduler-index.ts") }}"></script>
<script type="module" src="{{ static("bundled/reservation/slot-reservation-index.ts") }}"></script>
{% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static('core/components/calendar.scss') }}">
<link rel="stylesheet" href="{{ static('reservation/reservation.scss') }}">
{% endblock %}
{% block content %}
<h2 class="margin-bottom">{% trans %}Room reservation{% endtrans %}</h2>
<room-scheduler
locale="{{ LANGUAGE_CODE }}"
can_edit_slot="{{ user.has_perm("reservation.change_reservationslot") }}"
can_create_slot="{{ user.has_perm("reservation.add_reservationslot") }}"
></room-scheduler>
{% endblock %}

View File

@ -1,12 +1,14 @@
from django.urls import path from django.urls import path
from reservation.views import ( from reservation.views import (
ReservationScheduleView,
RoomCreateView, RoomCreateView,
RoomDeleteView, RoomDeleteView,
RoomUpdateView, RoomUpdateView,
) )
urlpatterns = [ urlpatterns = [
path("", ReservationScheduleView.as_view(), name="main"),
path("room/create/", RoomCreateView.as_view(), name="room_create"), path("room/create/", RoomCreateView.as_view(), name="room_create"),
path("room/<int:room_id>/edit", RoomUpdateView.as_view(), name="room_edit"), path("room/<int:room_id>/edit", RoomUpdateView.as_view(), name="room_edit"),
path("room/<int:room_id>/delete", RoomDeleteView.as_view(), name="room_delete"), path("room/<int:room_id>/delete", RoomDeleteView.as_view(), name="room_delete"),

View File

@ -8,10 +8,13 @@ from django.views.generic import CreateView, DeleteView, TemplateView, UpdateVie
from club.models import Club from club.models import Club
from core.auth.mixins import CanEditMixin from core.auth.mixins import CanEditMixin
from core.views import UseFragmentsMixin from reservation.forms import RoomCreateForm, RoomUpdateForm
from core.views.mixins import FragmentMixin from reservation.models import Room
from reservation.forms import ReservationForm, RoomCreateForm, RoomUpdateForm
from reservation.models import ReservationSlot, Room
class ReservationScheduleView(PermissionRequiredMixin, TemplateView):
template_name = "reservation/schedule.jinja"
permission_required = "reservation.view_room"
class RoomCreateView(SuccessMessageMixin, PermissionRequiredMixin, CreateView): class RoomCreateView(SuccessMessageMixin, PermissionRequiredMixin, CreateView):

View File

@ -17,7 +17,8 @@
"#core:*": ["./core/static/bundled/*"], "#core:*": ["./core/static/bundled/*"],
"#pedagogy:*": ["./pedagogy/static/bundled/*"], "#pedagogy:*": ["./pedagogy/static/bundled/*"],
"#counter:*": ["./counter/static/bundled/*"], "#counter:*": ["./counter/static/bundled/*"],
"#com:*": ["./com/static/bundled/*"] "#com:*": ["./com/static/bundled/*"],
"#reservation:*": ["./reservation/static/bundled/*"]
} }
} }
} }