mirror of
https://github.com/ae-utbm/sith.git
synced 2025-07-13 13:29:23 +00:00
Compare commits
11 Commits
ia-explana
...
counter-ac
Author | SHA1 | Date | |
---|---|---|---|
dae5cb06e7 | |||
113828f9b6 | |||
203b5d88ac | |||
9206fed4ce | |||
f133bac921 | |||
1bce7e055f | |||
ee19dc01f6 | |||
09dbda87bc | |||
a44e8a68cb | |||
71d155613f | |||
e30a6e8e6e |
@ -19,13 +19,16 @@ from django.db.models import Q
|
|||||||
from ninja import Query
|
from ninja import Query
|
||||||
from ninja_extra import ControllerBase, api_controller, paginate, route
|
from ninja_extra import ControllerBase, api_controller, paginate, route
|
||||||
from ninja_extra.pagination import PageNumberPaginationExtra
|
from ninja_extra.pagination import PageNumberPaginationExtra
|
||||||
|
from ninja_extra.permissions import IsAuthenticated
|
||||||
from ninja_extra.schemas import PaginatedResponseSchema
|
from ninja_extra.schemas import PaginatedResponseSchema
|
||||||
|
|
||||||
from core.api_permissions import CanAccessLookup, CanView, IsRoot
|
from core.api_permissions import CanAccessLookup, CanView, IsRoot
|
||||||
from counter.models import Counter, Product
|
from counter.models import Counter, Permanency, Product
|
||||||
from counter.schemas import (
|
from counter.schemas import (
|
||||||
CounterFilterSchema,
|
CounterFilterSchema,
|
||||||
CounterSchema,
|
CounterSchema,
|
||||||
|
PermanencyFilterSchema,
|
||||||
|
PermanencySchema,
|
||||||
ProductSchema,
|
ProductSchema,
|
||||||
SimplifiedCounterSchema,
|
SimplifiedCounterSchema,
|
||||||
)
|
)
|
||||||
@ -35,17 +38,15 @@ from counter.schemas import (
|
|||||||
class CounterController(ControllerBase):
|
class CounterController(ControllerBase):
|
||||||
@route.get("", response=list[CounterSchema], permissions=[IsRoot])
|
@route.get("", response=list[CounterSchema], permissions=[IsRoot])
|
||||||
def fetch_all(self):
|
def fetch_all(self):
|
||||||
return Counter.objects.annotate_is_open()
|
return Counter.objects.all()
|
||||||
|
|
||||||
@route.get("{counter_id}/", response=CounterSchema, permissions=[CanView])
|
@route.get("{counter_id}/", response=CounterSchema, permissions=[CanView])
|
||||||
def fetch_one(self, counter_id: int):
|
def fetch_one(self, counter_id: int):
|
||||||
return self.get_object_or_exception(
|
return self.get_object_or_exception(Counter.objects.all(), pk=counter_id)
|
||||||
Counter.objects.annotate_is_open(), pk=counter_id
|
|
||||||
)
|
|
||||||
|
|
||||||
@route.get("bar/", response=list[CounterSchema], permissions=[CanView])
|
@route.get("bar/", response=list[CounterSchema], permissions=[CanView])
|
||||||
def fetch_bars(self):
|
def fetch_bars(self):
|
||||||
counters = list(Counter.objects.annotate_is_open().filter(type="BAR"))
|
counters = list(Counter.objects.all().filter(type="BAR"))
|
||||||
for c in counters:
|
for c in counters:
|
||||||
self.check_object_permissions(c)
|
self.check_object_permissions(c)
|
||||||
return counters
|
return counters
|
||||||
@ -76,3 +77,21 @@ class ProductController(ControllerBase):
|
|||||||
.filter(archived=False)
|
.filter(archived=False)
|
||||||
.values()
|
.values()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_controller("/permanency")
|
||||||
|
class PermanencyController(ControllerBase):
|
||||||
|
@route.get(
|
||||||
|
"",
|
||||||
|
response=PaginatedResponseSchema[PermanencySchema],
|
||||||
|
permissions=[IsAuthenticated],
|
||||||
|
exclude_none=True,
|
||||||
|
)
|
||||||
|
@paginate(PageNumberPaginationExtra, page_size=100)
|
||||||
|
def fetch_permanencies(self, filters: Query[PermanencyFilterSchema]):
|
||||||
|
return (
|
||||||
|
filters.filter(Permanency.objects.all())
|
||||||
|
.distinct()
|
||||||
|
.order_by("-start")
|
||||||
|
.select_related("counter")
|
||||||
|
)
|
||||||
|
@ -1,19 +1,33 @@
|
|||||||
|
from datetime import datetime
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from annotated_types import MinLen
|
from annotated_types import MinLen
|
||||||
from ninja import Field, FilterSchema, ModelSchema
|
from ninja import Field, FilterSchema, ModelSchema
|
||||||
|
|
||||||
from core.schemas import SimpleUserSchema
|
from counter.models import Counter, Permanency, Product
|
||||||
from counter.models import Counter, Product
|
|
||||||
|
|
||||||
|
|
||||||
class CounterSchema(ModelSchema):
|
class CounterSchema(ModelSchema):
|
||||||
barmen_list: list[SimpleUserSchema]
|
|
||||||
is_open: bool
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Counter
|
model = Counter
|
||||||
fields = ["id", "name", "type", "club", "products"]
|
fields = ["id", "name", "type"]
|
||||||
|
|
||||||
|
|
||||||
|
class PermanencySchema(ModelSchema):
|
||||||
|
counter: CounterSchema
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Permanency
|
||||||
|
fields = ["start", "end"]
|
||||||
|
|
||||||
|
|
||||||
|
class PermanencyFilterSchema(FilterSchema):
|
||||||
|
start_after: datetime | None = Field(None, q="start__gte")
|
||||||
|
start_before: datetime | None = Field(None, q="start__lte")
|
||||||
|
end_after: datetime | None = Field(None, q="end__gte")
|
||||||
|
end_before: datetime | None = Field(None, q="end__lte")
|
||||||
|
took_place_after: datetime | None = Field(None, q=["start__gte", "end__gte"])
|
||||||
|
counter: set[int] | None = Field(None, q="counter_id__in")
|
||||||
|
|
||||||
|
|
||||||
class CounterFilterSchema(FilterSchema):
|
class CounterFilterSchema(FilterSchema):
|
||||||
|
152
counter/static/bundled/counter/permanencies/time-grid-index.ts
Normal file
152
counter/static/bundled/counter/permanencies/time-grid-index.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import { paginated } from "#core:utils/api";
|
||||||
|
import { exportToHtml } from "#core:utils/globals";
|
||||||
|
import { Calendar } from "@fullcalendar/core";
|
||||||
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
|
import {
|
||||||
|
type PermanencyFetchPermanenciesData,
|
||||||
|
type PermanencySchema,
|
||||||
|
permanencyFetchPermanencies,
|
||||||
|
} from "#openapi";
|
||||||
|
|
||||||
|
interface ActivityTimeGridConfig {
|
||||||
|
canvas: HTMLCanvasElement;
|
||||||
|
startDate: Date;
|
||||||
|
counterId: number;
|
||||||
|
locale: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpeningTime {
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventInput {
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
backgroundColor: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
exportToHtml("loadActivityTimeGrid", loadActivityTimeGrid);
|
||||||
|
|
||||||
|
async function loadActivityTimeGrid(options: ActivityTimeGridConfig) {
|
||||||
|
const permanencies = await paginated(permanencyFetchPermanencies, {
|
||||||
|
query: {
|
||||||
|
counter: [options.counterId],
|
||||||
|
// biome-ignore lint/style/useNamingConvention: backend API uses snake_case
|
||||||
|
took_place_after: options.startDate.toISOString(),
|
||||||
|
},
|
||||||
|
} as PermanencyFetchPermanenciesData);
|
||||||
|
|
||||||
|
const events = getEvents(permanencies);
|
||||||
|
|
||||||
|
const calendar = new Calendar(options.canvas, {
|
||||||
|
plugins: [timeGridPlugin],
|
||||||
|
initialView: "timeGridWeek",
|
||||||
|
locale: options.locale,
|
||||||
|
dayHeaderFormat: { weekday: "long" },
|
||||||
|
firstDay: 1,
|
||||||
|
views: { timeGrid: { allDaySlot: false } },
|
||||||
|
scrollTime: "09:00:00",
|
||||||
|
headerToolbar: { left: "prev today", center: "title", right: "" },
|
||||||
|
events: events,
|
||||||
|
nowIndicator: true,
|
||||||
|
height: 600,
|
||||||
|
});
|
||||||
|
calendar.render();
|
||||||
|
|
||||||
|
calendar.on("datesSet", async (info) => {
|
||||||
|
if (options.startDate <= info.start) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newPerms = await paginated(permanencyFetchPermanencies, {
|
||||||
|
query: {
|
||||||
|
counter: [options.counterId],
|
||||||
|
// biome-ignore lint/style/useNamingConvention: backend API uses snake_case
|
||||||
|
end_after: info.startStr,
|
||||||
|
// biome-ignore lint/style/useNamingConvention: backend API uses snake_case
|
||||||
|
start_before: info.endStr,
|
||||||
|
},
|
||||||
|
} as PermanencyFetchPermanenciesData);
|
||||||
|
options.startDate = info.start;
|
||||||
|
calendar.addEventSource(getEvents(newPerms, false));
|
||||||
|
permanencies.push(...newPerms);
|
||||||
|
calendar.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function roundToQuarter(date: Date, ceil: boolean) {
|
||||||
|
const result = date;
|
||||||
|
const minutes = date.getMinutes();
|
||||||
|
// removes minutes exceeding the lower quarter and adds 15 minutes if rounded to ceiling
|
||||||
|
result.setMinutes(minutes - (minutes % 15) + +ceil * 15, 0, 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertPermanencyToOpeningTime(permanency: PermanencySchema): OpeningTime {
|
||||||
|
return {
|
||||||
|
start: roundToQuarter(new Date(permanency.start), false),
|
||||||
|
end: roundToQuarter(new Date(permanency.end ?? Date.now()), true),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOpeningTimes(rawPermanencies: PermanencySchema[]) {
|
||||||
|
const permanencies = rawPermanencies
|
||||||
|
.map(convertPermanencyToOpeningTime)
|
||||||
|
.sort((a, b) => a.start.getTime() - b.start.getTime());
|
||||||
|
|
||||||
|
const openingTimes: OpeningTime[] = [];
|
||||||
|
|
||||||
|
for (const permanency of permanencies) {
|
||||||
|
// if there are no opening times, add the first one
|
||||||
|
if (openingTimes.length === 0) {
|
||||||
|
openingTimes.push(permanency);
|
||||||
|
} else {
|
||||||
|
const lastPermanency = openingTimes[openingTimes.length - 1];
|
||||||
|
// if the new permanency starts before the 15 minutes following the end of the last one, merge them
|
||||||
|
if (permanency.start <= lastPermanency.end) {
|
||||||
|
lastPermanency.end = new Date(
|
||||||
|
Math.max(lastPermanency.end.getTime(), permanency.end.getTime()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
openingTimes.push(permanency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return openingTimes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEvents(permanencies: PermanencySchema[], currentWeek = true): EventInput[] {
|
||||||
|
const openingTimes = getOpeningTimes(permanencies);
|
||||||
|
const events: EventInput[] = [];
|
||||||
|
for (const openingTime of openingTimes) {
|
||||||
|
let shift = false;
|
||||||
|
if (currentWeek) {
|
||||||
|
const lastMonday = getLastMonday();
|
||||||
|
shift = openingTime.end < lastMonday;
|
||||||
|
}
|
||||||
|
// if permanencies took place last week (=before monday),
|
||||||
|
// -> display them in lightblue as part of the current week
|
||||||
|
events.push({
|
||||||
|
start: shift ? shiftDateByDays(openingTime.start, 7) : openingTime.start,
|
||||||
|
end: shift ? shiftDateByDays(openingTime.end, 7) : openingTime.end,
|
||||||
|
backgroundColor: shift ? "lightblue" : "green",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get last Monday at 00:00
|
||||||
|
function getLastMonday(now = new Date()): Date {
|
||||||
|
const dayOfWeek = now.getDay();
|
||||||
|
const lastMonday = new Date(now);
|
||||||
|
lastMonday.setDate(now.getDate() - ((dayOfWeek + 6) % 7)); // Adjust for Monday as day 1
|
||||||
|
lastMonday.setHours(0, 0, 0, 0);
|
||||||
|
return lastMonday;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shiftDateByDays(date: Date, days: number): Date {
|
||||||
|
const newDate = new Date(date);
|
||||||
|
newDate.setDate(date.getDate() + days);
|
||||||
|
return newDate;
|
||||||
|
}
|
@ -22,3 +22,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#activityTimeGrid {
|
||||||
|
th {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-col-header-cell-cushion {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
table table {
|
||||||
|
// the JS library puts tables inside the table
|
||||||
|
// Those additional tables must be hidden
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,10 @@
|
|||||||
{% trans counter_name=counter %}{{ counter_name }} activity{% endtrans %}
|
{% trans counter_name=counter %}{{ counter_name }} activity{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block additional_js %}
|
||||||
|
<script defer src="{{ static('bundled/counter/permanencies/time-grid-index.ts') }}" type="module"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{%- block additional_css -%}
|
{%- block additional_css -%}
|
||||||
<link rel="stylesheet" href="{{ static('counter/css/activity.scss') }}">
|
<link rel="stylesheet" href="{{ static('counter/css/activity.scss') }}">
|
||||||
{%- endblock -%}
|
{%- endblock -%}
|
||||||
@ -22,6 +26,10 @@
|
|||||||
{% trans %}There is currently no barman connected.{% endtrans %}
|
{% trans %}There is currently no barman connected.{% endtrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
<h4>{% trans %}Last weeks opening times{% endtrans %}</h4>
|
||||||
|
<div id="activityTimeGrid"></div>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h5>{% trans %}Legend{% endtrans %}</h5>
|
<h5>{% trans %}Legend{% endtrans %}</h5>
|
||||||
@ -37,5 +45,17 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
{{super()}}
|
||||||
|
<script>
|
||||||
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
|
loadActivityTimeGrid({
|
||||||
|
canvas: document.getElementById("activityTimeGrid"),
|
||||||
|
// sets the start day to 7 days ago
|
||||||
|
startDate: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||||
|
counterId: {{ counter.id }},
|
||||||
|
locale: {{ get_current_language()|tojson }}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@ -6143,3 +6143,7 @@ msgstr "Vous ne pouvez plus écrire de commentaires, la date est passée."
|
|||||||
#, python-format
|
#, python-format
|
||||||
msgid "Maximum characters: %(max_length)s"
|
msgid "Maximum characters: %(max_length)s"
|
||||||
msgstr "Nombre de caractères max: %(max_length)s"
|
msgstr "Nombre de caractères max: %(max_length)s"
|
||||||
|
|
||||||
|
#: counter/activity.jinja
|
||||||
|
msgid "Last weeks opening times"
|
||||||
|
msgstr "Heures d'ouverture des dernières semaines"
|
613
package-lock.json
generated
613
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -34,6 +34,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||||
|
"@fullcalendar/core": "^6.1.15",
|
||||||
|
"@fullcalendar/timegrid": "^6.1.15",
|
||||||
"@hey-api/client-fetch": "^0.4.0",
|
"@hey-api/client-fetch": "^0.4.0",
|
||||||
"@sentry/browser": "^8.34.0",
|
"@sentry/browser": "^8.34.0",
|
||||||
"@zip.js/zip.js": "^2.7.52",
|
"@zip.js/zip.js": "^2.7.52",
|
||||||
|
@ -164,6 +164,7 @@ TEMPLATES = [
|
|||||||
"ProductType": "counter.models.ProductType",
|
"ProductType": "counter.models.ProductType",
|
||||||
"timezone": "django.utils.timezone",
|
"timezone": "django.utils.timezone",
|
||||||
"get_sith": "com.views.sith",
|
"get_sith": "com.views.sith",
|
||||||
|
"get_current_language": "django.views.i18n.get_language",
|
||||||
},
|
},
|
||||||
"bytecode_cache": {
|
"bytecode_cache": {
|
||||||
"name": "default",
|
"name": "default",
|
||||||
|
0
webpack.analyze.config.js
Normal file
0
webpack.analyze.config.js
Normal file
Reference in New Issue
Block a user