From 1bf4cc3125dc2c9c39241908cb9cd9ebf579bd8b Mon Sep 17 00:00:00 2001 From: HumanoidSandvichDispenser Date: Thu, 7 Nov 2024 17:58:26 -0800 Subject: [PATCH] Add timezone-aware schedule window --- availabili.tf/src/client/models/TeamSchema.ts | 2 + availabili.tf/src/stores/schedule.test.ts | 31 ++++++++++++++ availabili.tf/src/stores/schedule.ts | 42 +++++++++++++++---- availabili.tf/src/views/ScheduleView.vue | 7 ++-- backend-flask/models.py | 2 + backend-flask/team.py | 4 +- 6 files changed, 74 insertions(+), 14 deletions(-) create mode 100644 availabili.tf/src/stores/schedule.test.ts diff --git a/availabili.tf/src/client/models/TeamSchema.ts b/availabili.tf/src/client/models/TeamSchema.ts index 0df9a2a..8189561 100644 --- a/availabili.tf/src/client/models/TeamSchema.ts +++ b/availabili.tf/src/client/models/TeamSchema.ts @@ -5,6 +5,8 @@ export type TeamSchema = { discordWebhookUrl?: string; id: number; + minuteOffset: number; teamName: string; + tzTimezone: string; }; diff --git a/availabili.tf/src/stores/schedule.test.ts b/availabili.tf/src/stores/schedule.test.ts new file mode 100644 index 0000000..77eb609 --- /dev/null +++ b/availabili.tf/src/stores/schedule.test.ts @@ -0,0 +1,31 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { useScheduleStore } from "./schedule"; +import { createPinia, setActivePinia } from "pinia"; + +describe("Schedule store", () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + it("should return the proper window start for any timezone", () => { + const scheduleStore = useScheduleStore(); + + let test1 = scheduleStore.getWindowStart({ + teamName: "", + id: 0, + tzTimezone: "Asia/Kolkata", + minuteOffset: 10, + }); + + expect(test1.get("minutes")).toEqual(40); + + let test2 = scheduleStore.getWindowStart({ + teamName: "", + id: 0, + tzTimezone: "America/New_York", + minuteOffset: 30, + }); + + expect(test2.get("minutes")).toEqual(30); + }); +}); diff --git a/availabili.tf/src/stores/schedule.ts b/availabili.tf/src/stores/schedule.ts index ec0ac95..fef4c1b 100644 --- a/availabili.tf/src/stores/schedule.ts +++ b/availabili.tf/src/stores/schedule.ts @@ -3,6 +3,9 @@ import { defineStore } from "pinia"; import { reactive, ref, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; import { useClientStore } from "./client"; +import type { TeamSchema } from "@/client"; +import moment from "moment"; +import "moment-timezone"; export const useScheduleStore = defineStore("schedule", () => { const client = useClientStore().client; @@ -18,23 +21,43 @@ export const useScheduleStore = defineStore("schedule", () => { const route = useRoute(); const router = useRouter(); - const teamId = computed({ - get: () => Number(route.query.teamId), - set: (value) => router.push({ query: { teamId: value } }), - }); + //const teamId = computed({ + // get: () => Number(route?.query?.teamId), + // set: (value) => router.push({ query: { teamId: value } }), + //}); + const team = ref(); + + function getWindowStart(team: TeamSchema) { + // convert local 00:00 to league timezone + let localMidnight = moment().startOf("isoWeek"); + let leagueTime = localMidnight.clone().tz(team.tz_timezone); + + let nextMinuteOffsetTime = leagueTime.clone(); + + if (nextMinuteOffsetTime.minute() > team.minute_offset) { + nextMinuteOffsetTime.add(1, "hour"); + } + + nextMinuteOffsetTime.minute(team.minute_offset); + + const deltaMinutes = nextMinuteOffsetTime.diff(leagueTime, "minutes"); + + return localMidnight.clone().add(deltaMinutes, "minutes"); + } watch(dateStart, () => { fetchSchedule(); }); - watch(teamId, () => { - fetchSchedule(); + watch(team, () => { + dateStart.value = getWindowStart(team.value).toDate(); + console.log(dateStart.value); }); async function fetchSchedule() { return client.default.getApiSchedule( Math.floor(dateStart.value.getTime() / 1000).toString(), - teamId.value, + team.value.id, ) .then((response) => { response.availability.forEach((value, i) => { @@ -60,7 +83,7 @@ export const useScheduleStore = defineStore("schedule", () => { async function saveSchedule() { return client.default.putApiSchedule({ windowStart: Math.floor(dateStart.value.getTime() / 1000).toString(), - teamId: teamId.value, + teamId: team.value.id, availability, }); //return fetch(import.meta.env.VITE_API_BASE_URL + "/schedule", { @@ -83,6 +106,7 @@ export const useScheduleStore = defineStore("schedule", () => { availability, fetchSchedule, saveSchedule, - teamId, + team, + getWindowStart, }; }); diff --git a/availabili.tf/src/views/ScheduleView.vue b/availabili.tf/src/views/ScheduleView.vue index 8d9e7a1..c24c303 100644 --- a/availabili.tf/src/views/ScheduleView.vue +++ b/availabili.tf/src/views/ScheduleView.vue @@ -30,7 +30,7 @@ const selectedTeam = ref(); watch(selectedTeam, (newTeam) => { if (newTeam) { - schedule.teamId = newTeam.id; + schedule.team = newTeam; } }); @@ -47,11 +47,10 @@ onMounted(() => { options.value = Object.values(teamsList.teams); // select team with id in query parameter if exists const queryTeam = teamsList.teams.find(x => x.id == route.query.teamId); - console.log(queryTeam); if (queryTeam) { selectedTeam.value = queryTeam; - //schedule.teamId = queryTeam.id; - schedule.fetchSchedule(schedule.teamId); + schedule.team = queryTeam; + schedule.fetchSchedule(); } }); }); diff --git a/backend-flask/models.py b/backend-flask/models.py index 1a6a64f..07a1a38 100644 --- a/backend-flask/models.py +++ b/backend-flask/models.py @@ -57,6 +57,8 @@ class TeamSchema(spec.BaseModel): id: int team_name: str discord_webhook_url: str | None + tz_timezone: str + minute_offset: int #players: list[PlayerTeamSpec] | None class PlayerTeam(db.Model): diff --git a/backend-flask/team.py b/backend-flask/team.py index 45d7b65..88aa32c 100644 --- a/backend-flask/team.py +++ b/backend-flask/team.py @@ -20,7 +20,9 @@ def map_team_to_schema(team: Team): return TeamSchema( id=team.id, team_name=team.team_name, - discord_webhook_url=None + discord_webhook_url=None, + tz_timezone=team.tz_timezone, + minute_offset=team.minute_offset ) def map_player_to_schema(player: Player):