mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-11-03 18:43:04 +00:00 
			
		
		
		
	Compare commits
	
		
			11 Commits
		
	
	
		
			taiste
			...
			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_extra import ControllerBase, api_controller, paginate, route
 | 
			
		||||
from ninja_extra.pagination import PageNumberPaginationExtra
 | 
			
		||||
from ninja_extra.permissions import IsAuthenticated
 | 
			
		||||
from ninja_extra.schemas import PaginatedResponseSchema
 | 
			
		||||
 | 
			
		||||
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 (
 | 
			
		||||
    CounterFilterSchema,
 | 
			
		||||
    CounterSchema,
 | 
			
		||||
    PermanencyFilterSchema,
 | 
			
		||||
    PermanencySchema,
 | 
			
		||||
    ProductSchema,
 | 
			
		||||
    SimplifiedCounterSchema,
 | 
			
		||||
)
 | 
			
		||||
@@ -35,17 +38,15 @@ from counter.schemas import (
 | 
			
		||||
class CounterController(ControllerBase):
 | 
			
		||||
    @route.get("", response=list[CounterSchema], permissions=[IsRoot])
 | 
			
		||||
    def fetch_all(self):
 | 
			
		||||
        return Counter.objects.annotate_is_open()
 | 
			
		||||
        return Counter.objects.all()
 | 
			
		||||
 | 
			
		||||
    @route.get("{counter_id}/", response=CounterSchema, permissions=[CanView])
 | 
			
		||||
    def fetch_one(self, counter_id: int):
 | 
			
		||||
        return self.get_object_or_exception(
 | 
			
		||||
            Counter.objects.annotate_is_open(), pk=counter_id
 | 
			
		||||
        )
 | 
			
		||||
        return self.get_object_or_exception(Counter.objects.all(), pk=counter_id)
 | 
			
		||||
 | 
			
		||||
    @route.get("bar/", response=list[CounterSchema], permissions=[CanView])
 | 
			
		||||
    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:
 | 
			
		||||
            self.check_object_permissions(c)
 | 
			
		||||
        return counters
 | 
			
		||||
@@ -76,3 +77,21 @@ class ProductController(ControllerBase):
 | 
			
		||||
            .filter(archived=False)
 | 
			
		||||
            .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 annotated_types import MinLen
 | 
			
		||||
from ninja import Field, FilterSchema, ModelSchema
 | 
			
		||||
 | 
			
		||||
from core.schemas import SimpleUserSchema
 | 
			
		||||
from counter.models import Counter, Product
 | 
			
		||||
from counter.models import Counter, Permanency, Product
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CounterSchema(ModelSchema):
 | 
			
		||||
    barmen_list: list[SimpleUserSchema]
 | 
			
		||||
    is_open: bool
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        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):
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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 %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block additional_js %}
 | 
			
		||||
  <script defer src="{{ static('bundled/counter/permanencies/time-grid-index.ts') }}" type="module"></script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{%- block additional_css -%}
 | 
			
		||||
  <link rel="stylesheet" href="{{ static('counter/css/activity.scss') }}">
 | 
			
		||||
{%- endblock -%}
 | 
			
		||||
@@ -22,6 +26,10 @@
 | 
			
		||||
        {% trans %}There is currently no barman connected.{% endtrans %}
 | 
			
		||||
      {% endif %}
 | 
			
		||||
    </ul>
 | 
			
		||||
    <h4>{% trans %}Last weeks opening times{% endtrans %}</h4>
 | 
			
		||||
    <div id="activityTimeGrid"></div>
 | 
			
		||||
    <br/>
 | 
			
		||||
    <br/>
 | 
			
		||||
  {% endif %}
 | 
			
		||||
 | 
			
		||||
  <h5>{% trans %}Legend{% endtrans %}</h5>
 | 
			
		||||
@@ -37,5 +45,17 @@
 | 
			
		||||
  </div>
 | 
			
		||||
{% 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
 | 
			
		||||
msgid "Maximum characters: %(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": {
 | 
			
		||||
    "@fortawesome/fontawesome-free": "^6.6.0",
 | 
			
		||||
    "@fullcalendar/core": "^6.1.15",
 | 
			
		||||
    "@fullcalendar/timegrid": "^6.1.15",
 | 
			
		||||
    "@hey-api/client-fetch": "^0.4.0",
 | 
			
		||||
    "@sentry/browser": "^8.34.0",
 | 
			
		||||
    "@zip.js/zip.js": "^2.7.52",
 | 
			
		||||
 
 | 
			
		||||
@@ -164,6 +164,7 @@ TEMPLATES = [
 | 
			
		||||
                "ProductType": "counter.models.ProductType",
 | 
			
		||||
                "timezone": "django.utils.timezone",
 | 
			
		||||
                "get_sith": "com.views.sith",
 | 
			
		||||
                "get_current_language": "django.views.i18n.get_language",
 | 
			
		||||
            },
 | 
			
		||||
            "bytecode_cache": {
 | 
			
		||||
                "name": "default",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								webpack.analyze.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								webpack.analyze.config.js
									
									
									
									
									
										Normal file
									
								
							
		Reference in New Issue
	
	Block a user