Skip to content

Commit

Permalink
Created new API view for retrieving the current active shift, added t…
Browse files Browse the repository at this point in the history
…emplates for displaying shift information on a screen in the canteens
  • Loading branch information
vincevd1 committed Oct 22, 2024
1 parent f79830a commit 1f128de
Show file tree
Hide file tree
Showing 9 changed files with 496 additions and 6 deletions.
2 changes: 2 additions & 0 deletions website/orders/api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
ProductListAPIView,
ShiftRetrieveUpdateAPIView,
OrderVenueListAPIView,
OrderVenueActiveShiftAPIView
)
from orders.converters import ShiftConverter, OrderConverter

Expand All @@ -26,4 +27,5 @@
path("shifts/<shift:shift>/scanner/", ShiftScannerAPIView.as_view(), name="shifts_scanner"),
path("shifts/<shift:shift>/products/", ProductListAPIView.as_view(), name="product_list"),
path("order-venues/", OrderVenueListAPIView.as_view(), name="ordervenues_list"),
path("order-venues/<order_venue:order_venue>/active-shift", OrderVenueActiveShiftAPIView.as_view(), name="ordervenues_activeshift"),
]
15 changes: 15 additions & 0 deletions website/orders/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
from tosti.api.views import LoggedRetrieveUpdateAPIView, LoggedListCreateAPIView, LoggedRetrieveUpdateDestroyAPIView
from tosti.utils import log_action

from orders.templatetags.start_shift import currently_active_shift_for_venue


class OrderListCreateAPIView(ListCreateAPIView):
"""API View to list and create orders."""
Expand Down Expand Up @@ -298,3 +300,16 @@ class OrderVenueListAPIView(ListAPIView):

serializer_class = OrderVenueSerializer
queryset = OrderVenue.objects.all()

class OrderVenueActiveShiftAPIView(APIView):
"""API View to retrieve the active shift"""

serializer_class = ShiftSerializer

def get(self, request, **kwargs):
shift = currently_active_shift_for_venue(kwargs.get("order_venue"))

if shift:
return Response(status=status.HTTP_200_OK, data=self.serializer_class(shift).data)
else:
return Response(status=status.HTTP_404_NOT_FOUND)
8 changes: 4 additions & 4 deletions website/orders/templatetags/start_shift.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ def currently_active_shift_for_venue(venue: OrderVenue):
today = timezone.localize(datetime.now())
start = timezone.localize(datetime(today.year, today.month, today.day))

if venue.shifts.filter(start__lte=today, end__gte=today).exists():
return venue.shifts.filter(start__lte=today, end__gte=today).first()
elif venue.shifts.filter(end__gte=start).exists():
return venue.shifts.filter(end__gte=start).order_by("-end").first()
if venue.shifts.filter(start__lte=today, end__gte=today, finalized=False).exists():
return venue.shifts.filter(start__lte=today, end__gte=today, finalized=False).first()
elif venue.shifts.filter(end__gte=start, finalized=False).exists():
return venue.shifts.filter(end__gte=start, finalized=False).order_by("-end").first()
else:
return None
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
justify-content: center;
}

.orders-ready li.user-order-item {
background-color: var(--success);
}

.user-order-list li .order-user-name {
margin-right: 20px;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<h2>In progress</h2>
<transition-group name="user_objects_with_orders_in_progress" tag="ul" class="user-order-list orders-in-progress px-5">
<li v-for="(user_orders_object, user_id) in orders_in_progress_grouped_user" :key="`orders_in_progress_user_${user_id}`" class="user-order-item">
<div class="order-user-name">${user_orders_object.user.first_name}$</div>
<div class="order-user-name">${user_orders_object.user.first_name}$ ${user_orders_object.user.last_name}$</div>
<ul class="order-list">
<li v-for="order in user_orders_object['orders']" :key="`user_${user_id}_order_${order.id}`" class="order-item">
<i v-if="order.product.icon !== null" :class="`fa-solid fa-${order.product.icon} me-2`"></i>
Expand All @@ -42,7 +42,7 @@ <h2>In progress</h2>
<h2>Ready</h2>
<transition-group name="orders_ready" tag="ul" class="user-order-list orders-ready px-5">
<li v-for="(user_orders_object, user_id) in orders_ready_grouped_user" :key="`orders_ready_user_${user_id}`" class="user-order-item">
<div class="order-user-name">${user_orders_object.user.first_name}$</div>
<div class="order-user-name">${user_orders_object.user.first_name}$ ${user_orders_object.user.last_name}$</div>
<ul class="order-list">
<li v-for="order in user_orders_object['orders']" :key="`user_${user_id}_order_${order.id}`" class="order-item">
<i v-if="order.product.icon !== null" :class="`fa-solid fa-${order.product.icon} me-2`"></i>
Expand Down
234 changes: 234 additions & 0 deletions website/status_screen/templates/status_screen/venue_music_screen.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
{% extends 'tosti/base.html' %}
{% load static players %}

{% block styles %}
<link rel="stylesheet" href="{% static 'status_screen/css/status-screen.css' %}"/>
<link rel="stylesheet" href="{% static 'thaliedje/css/player.css' %}"/>
{% endblock %}

{% block header %}
<nav class="navbar navbar-expand-lg site-header sticky-top navbar-dark">
<div class="container">
<div class="d-block d-lg-none">
<a class="navbar-brand drop-out-header-mobile" href="/"><img
src="{% static 'tosti/svg/TOSTI-logo.svg' %}" height="80"/></a>
</div>
<div class="mx-auto position-relative d-lg-block d-none" style="margin-top: -68px">
<a class="navbar-brand drop-out-header" href="/"><img
src="{% static 'tosti/svg/TOSTI-logo.svg' %}"
height="120"/>
</a>
</div>
</div>
</nav>
{% endblock %}

{% block page %}
<div class="d-flex flex-column justify-content-center align-items-center mt-5" id="music-screen-container">
<h1 class="mt-5">Currently playing:</h1>
<div class="player" id="player-container" style="width: 65%">
<template v-if="player_data.track.name !== null">
<div class="w-50 mx-auto mb-2" v-if="player_data.track.image !== null">
<a href="{% url "thaliedje:now_playing" player=player %}">
<img class="w-100" :src="player_data.track.image" :alt="player_data.track.name"/>
</a>
</div>
<div class="w-50 mx-auto mb-3" v-if="this.track_progress_ms !== null && this.track_progress_percentage !== null && this.player_data !== null && this.player_data.duration_ms !== null">
<div class="progress" style="height: 10px">
<div
class="progress-bar"
role="progressbar"
:style="'width: ' + this.track_progress_percentage+'%;'"
:aria-valuenow="this.track_progress_ms"
aria-valuemin="0"
:aria-valuemax="this.player_data.duration_ms">
</div>
</div>
<div>
<h4 class="float-start me-2 align-left text-nowrap" style="width: 30px">
${ Math.floor((track_progress_ms / 1000) / 60) }$:${ String(Math.floor((track_progress_ms / 1000) % 60)).padStart(2, '0') }$
</h4>
<h4 class="float-end ms-2 align-right">
${ Math.floor((player_data.duration_ms / 1000) / 60) }$:${ String(Math.floor((player_data.duration_ms / 1000) % 60)).padStart(2, '0') }$
<!--(${ -1 * Math.floor((track_ms_left / 1000) / 60) }$:${ String(Math.floor((track_ms_left / 1000) % 60)).padStart(2, '0') }$) -->
</h4>
</div>
</div>
<h2 class="text-center m-auto mt-5">${ player_data.track.name }$</h2>
<div class="text-center w-100">
<h3>
<template v-for="(artist, index) in player_data.track.artists">
<template v-if="index === player_data.track.artists.length - 1">
${ artist }$
</template>
<template v-else>
${ artist }$ -
</template>
</template>
</h3>
</div>
</template>
<template v-else>
<h1 class="text-center m-auto">No currently playing track</h1>
</template>
</div>
{% endblock %}

{% block footer %}

{% endblock %}

{% block js %}
<script>
const MUSIC_SCREEN_CONTAINER_ID = "music-screen-container";

window.player_vue = createApp({
delimiters: ['${', '}$'],
data() {
return {
player_data: {
is_playing: false,
shuffle: null,
track: {
image: null,
name: null,
artists: [],
},
current_volume: null,
timestamp: null,
progress_ms: null,
duration_ms: null,
},
doing_call: false,
set_volume_index: 0,
refresh_timer: null,
recalculate_progress_interval: null,
track_progress_ms: 0,
track_progress_percentage: 0,
track_ms_left: 0,
lastRefresh: null,
refreshing: false,
}
},
created() {
this.refresh();
this.recalculate_progress_interval = setInterval(this.updateTrackProgress, 100);
document.addEventListener("visibilitychange", this.visibilityChange);
},
unmounted() {
document.removeEventListener("visibilitychange", this.visibilityChange);
},
watch: {
track_progress_percentage: {
handler(newValue, oldValue) {
if (newValue.track_progress_percentage === 100 && oldValue.track_progress_percentage !== 100) {
this.refresh();
}
}
}
},
methods: {
visibilityChange(event) {
if (event.target.hidden) {
clearTimeout(this.refresh_timer);
clearInterval(this.recalculate_progress_interval);
} else {
clearTimeout(this.refresh_timer);
clearInterval(this.recalculate_progress_interval);
this.recalculate_progress_interval = setInterval(this.updateTrackProgress, 100);
if (this.lastRefresh === null || (new Date()).getTime() - this.lastRefresh > 5000) {
this.refresh();
} else {
this.refresh_timer = setTimeout(this.refresh, this.track_progress_percentage === 100 ? 1000 : 5000);
}
}
},
updateTrackProgress() {
if (this.player_data.is_playing === false) {
return;
}

if (this.player_data.progress_ms === null || this.player_data.timestamp === null) {
return;
}

const track_progress_ms = this.player_data.progress_ms + (Date.now() - this.player_data.timestamp);

if (track_progress_ms >= this.player_data.duration_ms) {
// Track finished playing.
this.track_progress_ms = this.player_data.duration_ms;
this.track_progress_percentage = 100;
this.track_ms_left = 0;
} else {
this.track_progress_ms = track_progress_ms;
this.track_progress_percentage = this.track_progress_ms / this.player_data.duration_ms * 100;
this.track_ms_left = this.player_data.duration_ms - this.track_progress_ms;
}
},
refresh() {
if (this.refreshing) {
return;
}

clearTimeout(this.refresh_timer);
this.refreshing = true;
return new Promise((resolve, reject) => {
fetch(
'{% url "v1:player_retrieve" pk=player.id %}',
{
headers: {
"X-CSRFToken": get_csrf_token(),
"Content-Type": 'application/json',
"Accept": 'application/json',
}
}
).then(response => {
if (response.status === 200) {
return response.json();
} else {
reject(response)
}
}).then(data => {
if (data.current_volume === null) {
data.current_volume = 50;
}
this.player_data = data;
}).catch(error => {
reject(`An error occurred while refreshing player {{ player.id }}. Error: ${error}`)
})

fetch(
"{% url 'v1:ordervenues_activeshift' order_venue=order_venue %}",
{
method: 'GET',
headers: {
"X-CSRFToken": get_csrf_token(),
"Accept": 'application/json',
}
}
).then(response => {
/* We want to change the page to the shift page again when the shift becomes active
So if the response on the active shift api call is 200 we reload the page
*/
if (response.status === 200) {
location.reload();
}

resolve();
}).catch(error => {
reject(`An error occurred while refreshing orders for venue {{ order_venue.name }}. Error: ${error}`)
})
})
.catch(error => {
console.log(error);
})
.finally(() => {
this.lastRefresh = (new Date()).getTime();
this.refreshing = false;
this.refresh_timer = setTimeout(this.refresh, this.track_progress_percentage === 100 ? 1000 : 5000);
});
}
}
}).mount(`#${MUSIC_SCREEN_CONTAINER_ID}`);
</script>
{% endblock %}
Loading

0 comments on commit 1f128de

Please sign in to comment.