Skip to content

Commit 5b29250

Browse files
committed
Room reservation form
1 parent de7caea commit 5b29250

14 files changed

Lines changed: 215 additions & 6 deletions

File tree

core/static/bundled/alpine-index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { morph } from "@alpinejs/morph";
12
import sort from "@alpinejs/sort";
23
import Alpine from "alpinejs";
34

4-
Alpine.plugin(sort);
5+
Alpine.plugin([sort, morph]);
56
window.Alpine = Alpine;
67

78
window.addEventListener("DOMContentLoaded", () => {

core/static/bundled/htmx-index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import htmx from "htmx.org";
2+
import "htmx-ext-alpine-morph";
23

34
document.body.addEventListener("htmx:beforeRequest", (event) => {
45
event.target.ariaBusy = true;

core/views/mixins.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def some_view(request):
109109
return render(
110110
request,
111111
"app/template.jinja",
112-
context={"fragment": fragment(request)
112+
context={"fragment": fragment(request)}
113113
}
114114
115115
# in urls.py

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"vite-plugin-static-copy": "^3.0.2"
4141
},
4242
"dependencies": {
43+
"@alpinejs/morph": "^3.14.9",
4344
"@alpinejs/sort": "^3.14.7",
4445
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
4546
"@floating-ui/dom": "^1.6.13",
@@ -64,6 +65,7 @@
6465
"d3-force-3d": "^3.0.5",
6566
"easymde": "^2.19.0",
6667
"glob": "^11.0.0",
68+
"htmx-ext-alpine-morph": "^2.0.1",
6769
"htmx.org": "^2.0.3",
6870
"jquery": "^3.7.1",
6971
"js-cookie": "^3.0.5",

reservation/forms.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from club.widgets.ajax_select import AutoCompleteSelectClub
44
from core.models import User
5-
from reservation.models import Room
5+
from core.views.forms import FutureDateTimeField, SelectDateTime
6+
from reservation.models import ReservationSlot, Room
67

78

89
class RoomCreateForm(forms.ModelForm):
@@ -31,3 +32,22 @@ def __init__(self, *args, request_user: User, **kwargs):
3132
# (i.e. it's a club board member, but not a sith admin)
3233
# some fields aren't editable
3334
del self.fields["club"]
35+
36+
37+
class ReservationForm(forms.ModelForm):
38+
required_css_class = "required"
39+
error_css_class = "error"
40+
41+
class Meta:
42+
model = ReservationSlot
43+
fields = ["room", "start_at", "end_at", "comment"]
44+
field_classes = {"start_at": FutureDateTimeField, "end_at": FutureDateTimeField}
45+
widgets = {"start_at": SelectDateTime(), "end_at": SelectDateTime()}
46+
47+
def __init__(self, *args, author: User, **kwargs):
48+
super().__init__(*args, **kwargs)
49+
self.author = author
50+
51+
def save(self, commit: bool = True): # noqa FBT001
52+
self.instance.author = self.author
53+
return super().save(commit)

reservation/static/bundled/reservation/components/room-scheduler-index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { inheritHtmlElement, registerComponent } from "#core:utils/web-components";
22
import {
33
Calendar,
4+
type DateSelectArg,
45
type EventDropArg,
56
type EventSourceFuncArg,
67
} from "@fullcalendar/core";
@@ -18,6 +19,7 @@ import {
1819
import { paginated } from "#core:utils/api";
1920
import interactionPlugin from "@fullcalendar/interaction";
2021
import resourceTimelinePlugin from "@fullcalendar/resource-timeline";
22+
import type { SlotSelectedEventArg } from "#reservation:reservation/types";
2123

2224
@registerComponent("room-scheduler")
2325
export class RoomScheduler extends inheritHtmlElement("div") {
@@ -26,6 +28,7 @@ export class RoomScheduler extends inheritHtmlElement("div") {
2628
private locale = "en";
2729
private canEditSlot = false;
2830
private canBookSlot = false;
31+
private canDeleteSlot = false;
2932

3033
attributeChangedCallback(name: string, _oldValue?: string, newValue?: string) {
3134
if (name === "locale") {
@@ -37,6 +40,9 @@ export class RoomScheduler extends inheritHtmlElement("div") {
3740
if (name === "can_create_slot") {
3841
this.canBookSlot = newValue.toLowerCase() === "true";
3942
}
43+
if (name === "can_delete_slot") {
44+
this.canDeleteSlot = newValue.toLowerCase() === "true";
45+
}
4046
}
4147

4248
/**
@@ -84,6 +90,18 @@ export class RoomScheduler extends inheritHtmlElement("div") {
8490
}
8591
}
8692

93+
selectFreeSlot(infos: DateSelectArg) {
94+
document.dispatchEvent(
95+
new CustomEvent<SlotSelectedEventArg>("timeSlotSelected", {
96+
detail: {
97+
ressource: Number.parseInt(infos.resource.id),
98+
start: infos.startStr,
99+
end: infos.endStr,
100+
},
101+
}),
102+
);
103+
}
104+
87105
connectedCallback() {
88106
super.connectedCallback();
89107
this.scheduler = new Calendar(this.node, {
@@ -109,6 +127,7 @@ export class RoomScheduler extends inheritHtmlElement("div") {
109127
resourceAreaWidth: "20%",
110128
resources: this.fetchResources,
111129
events: this.fetchEvents,
130+
select: this.selectFreeSlot,
112131
selectOverlap: false,
113132
selectable: this.canBookSlot,
114133
selectConstraint: { start: new Date() },
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { SlotSelectedEventArg } from "#reservation:reservation/types";
2+
3+
document.addEventListener("alpine:init", () => {
4+
Alpine.data("slotReservation", () => ({
5+
start: null as string,
6+
end: null as string,
7+
room: null as number,
8+
showForm: false,
9+
10+
init() {
11+
document.addEventListener(
12+
"timeSlotSelected",
13+
(event: CustomEvent<SlotSelectedEventArg>) => {
14+
this.start = event.detail.start.split("+")[0];
15+
this.end = event.detail.end.split("+")[0];
16+
this.room = event.detail.ressource;
17+
this.showForm = true;
18+
this.$nextTick(() => this.$el.scrollIntoView({ behavior: "smooth" })).then();
19+
},
20+
);
21+
},
22+
}));
23+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface SlotSelectedEventArg {
2+
start: string;
3+
end: string;
4+
ressource: number;
5+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#slot-reservation {
2+
margin-top: 3em;
3+
4+
display: flex;
5+
flex-direction: column;
6+
justify-content: center;
7+
8+
h3 {
9+
display: block;
10+
margin: auto;
11+
text-align: left;
12+
}
13+
14+
.alert, .error {
15+
display: block;
16+
margin: 1em auto auto;
17+
max-width: 400px;
18+
word-wrap: break-word;
19+
text-wrap: wrap;
20+
}
21+
22+
form {
23+
display: flex;
24+
flex-direction: column;
25+
gap: .5em;
26+
justify-content: center;
27+
28+
.buttons-row {
29+
input[type="submit"], button {
30+
margin: 0;
31+
}
32+
}
33+
textarea {
34+
max-width: unset;
35+
width: 100%;
36+
margin-top: unset;
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)