Implement role based shifts for crew
This commit is contained in:
parent
f69ca520c0
commit
a9ba0c55e1
3 changed files with 221 additions and 21 deletions
|
@ -68,6 +68,24 @@
|
||||||
{{ row.title }}
|
{{ row.title }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<template v-if="schedule.roles">
|
||||||
|
<tr>
|
||||||
|
<th>Shifts</th>
|
||||||
|
<td :colSpan="totalColumns"></td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="role in schedule.roles" :key="role.id">
|
||||||
|
<th>{{ role.name }}</th>
|
||||||
|
<td
|
||||||
|
v-for="row, index in roleRows.get(role.id)"
|
||||||
|
:key="index"
|
||||||
|
:colSpan="row.span"
|
||||||
|
:class='{"shift": row.slots.size }'
|
||||||
|
:title="row.title"
|
||||||
|
>
|
||||||
|
{{ row.title }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</figure>
|
</figure>
|
||||||
|
@ -75,7 +93,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import type { ScheduleEvent, ScheduleLocation, TimeSlot } from "~/shared/types/schedule";
|
import type { Role, ScheduleEvent, ScheduleLocation, Shift, ShiftSlot, TimeSlot } from "~/shared/types/schedule";
|
||||||
|
|
||||||
const oneDayMs = 24 * 60 * 60 * 1000;
|
const oneDayMs = 24 * 60 * 60 * 1000;
|
||||||
const oneHourMs = 60 * 60 * 1000;
|
const oneHourMs = 60 * 60 * 1000;
|
||||||
|
@ -84,7 +102,10 @@ const oneMinMs = 60 * 1000;
|
||||||
// See timetable-terminology.png for an illustration of how these terms are related
|
// See timetable-terminology.png for an illustration of how these terms are related
|
||||||
|
|
||||||
/** Point in time where a time slots starts or ends. */
|
/** Point in time where a time slots starts or ends. */
|
||||||
type Edge = { type: "start" | "end", slot: TimeSlot };
|
type Edge =
|
||||||
|
| { type: "start" | "end", source: "event", slot: TimeSlot }
|
||||||
|
| { type: "start" | "end", source: "shift", role: string, slot: ShiftSlot }
|
||||||
|
;
|
||||||
|
|
||||||
/** Point in time where multiple edges meet. */
|
/** Point in time where multiple edges meet. */
|
||||||
type Junction = { ts: number, edges: Edge[] };
|
type Junction = { ts: number, edges: Edge[] };
|
||||||
|
@ -94,6 +115,7 @@ type Span = {
|
||||||
start: Junction;
|
start: Junction;
|
||||||
end: Junction,
|
end: Junction,
|
||||||
locations: Map<string, Set<TimeSlot>>,
|
locations: Map<string, Set<TimeSlot>>,
|
||||||
|
roles: Map<string, Set<ShiftSlot>>,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -161,8 +183,20 @@ function* edgesFromEvents(events: Iterable<ScheduleEvent>): Generator<Edge> {
|
||||||
if (slot.start > slot.end) {
|
if (slot.start > slot.end) {
|
||||||
throw new Error(`Slot ${slot.id} ends before it starts.`);
|
throw new Error(`Slot ${slot.id} ends before it starts.`);
|
||||||
}
|
}
|
||||||
yield { type: "start", slot }
|
yield { type: "start", source: "event", slot }
|
||||||
yield { type: "end", slot }
|
yield { type: "end", source: "event", slot }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function* edgesFromShifts(shifts: Iterable<Shift>): Generator<Edge> {
|
||||||
|
for (const shift of shifts) {
|
||||||
|
for (const slot of shift.slots) {
|
||||||
|
if (slot.start > slot.end) {
|
||||||
|
throw new Error(`Slot ${slot.id} ends before it starts.`);
|
||||||
|
}
|
||||||
|
yield { type: "start", source: "shift", role: shift.role, slot };
|
||||||
|
yield { type: "end", source: "shift", role: shift.role, slot };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -183,17 +217,24 @@ function junctionsFromEdges(edges: Iterable<Edge>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function* spansFromJunctions(
|
function* spansFromJunctions(
|
||||||
junctions: Iterable<Junction>, locations: ScheduleLocation[]
|
junctions: Iterable<Junction>, locations: ScheduleLocation[], roles: Role[] | undefined,
|
||||||
): Generator<Span> {
|
): Generator<Span> {
|
||||||
const activeLocations = new Map(
|
const activeLocations = new Map(
|
||||||
locations.map(location => [location.id, new Set<TimeSlot>()])
|
locations.map(location => [location.id, new Set<TimeSlot>()])
|
||||||
);
|
);
|
||||||
|
const activeRoles = new Map(
|
||||||
|
roles?.map(role => [role.id, new Set<ShiftSlot>()])
|
||||||
|
);
|
||||||
for (const [start, end] of pairs(junctions)) {
|
for (const [start, end] of pairs(junctions)) {
|
||||||
for (const edge of start.edges) {
|
for (const edge of start.edges) {
|
||||||
if (edge.type === "start") {
|
if (edge.type === "start") {
|
||||||
|
if (edge.source === "event") {
|
||||||
for (const location of edge.slot.locations) {
|
for (const location of edge.slot.locations) {
|
||||||
activeLocations.get(location)?.add(edge.slot)
|
activeLocations.get(location)?.add(edge.slot)
|
||||||
}
|
}
|
||||||
|
} else if (edge.source === "shift") {
|
||||||
|
activeRoles.get(edge.role)?.add(edge.slot)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
yield {
|
yield {
|
||||||
|
@ -204,12 +245,21 @@ function* spansFromJunctions(
|
||||||
.filter(([_, slots]) => slots.size)
|
.filter(([_, slots]) => slots.size)
|
||||||
.map(([location, slots]) => [location, new Set(slots)])
|
.map(([location, slots]) => [location, new Set(slots)])
|
||||||
),
|
),
|
||||||
|
roles: new Map(
|
||||||
|
[...activeRoles]
|
||||||
|
.filter(([_, slots]) => slots.size)
|
||||||
|
.map(([role, slots]) => [role, new Set(slots)])
|
||||||
|
),
|
||||||
}
|
}
|
||||||
for (const edge of end.edges) {
|
for (const edge of end.edges) {
|
||||||
if (edge.type === "end") {
|
if (edge.type === "end") {
|
||||||
|
if (edge.source === "event") {
|
||||||
for (const location of edge.slot.locations) {
|
for (const location of edge.slot.locations) {
|
||||||
activeLocations.get(location)?.delete(edge.slot)
|
activeLocations.get(location)?.delete(edge.slot)
|
||||||
}
|
}
|
||||||
|
} else if (edge.source === "shift") {
|
||||||
|
activeRoles.get(edge.role)?.delete(edge.slot);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -228,7 +278,9 @@ function* stretchesFromSpans(spans: Iterable<Span>, minSeparation: number): Gene
|
||||||
for (const span of spans) {
|
for (const span of spans) {
|
||||||
// Based on how spans are generated I can assume that an empty span
|
// Based on how spans are generated I can assume that an empty span
|
||||||
// will only occur between two spans with timeslots in them.
|
// will only occur between two spans with timeslots in them.
|
||||||
if (span.locations.size === 0
|
if (
|
||||||
|
span.locations.size === 0
|
||||||
|
&& span.roles.size === 0
|
||||||
&& span.end.ts - span.start.ts >= minSeparation
|
&& span.end.ts - span.start.ts >= minSeparation
|
||||||
) {
|
) {
|
||||||
yield createStretch(currentSpans);
|
yield createStretch(currentSpans);
|
||||||
|
@ -259,6 +311,7 @@ function* cutSpansByHours(span: Span, timezone: string): Generator<Span> {
|
||||||
start: span.start,
|
start: span.start,
|
||||||
end: { ts: currentEnd.toMillis(), edges: [] },
|
end: { ts: currentEnd.toMillis(), edges: [] },
|
||||||
locations: span.locations,
|
locations: span.locations,
|
||||||
|
roles: span.roles,
|
||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
@ -271,6 +324,7 @@ function* cutSpansByHours(span: Span, timezone: string): Generator<Span> {
|
||||||
start: { ts: currentStart.toMillis(), edges: [] },
|
start: { ts: currentStart.toMillis(), edges: [] },
|
||||||
end: { ts: currentEnd.toMillis(), edges: [] },
|
end: { ts: currentEnd.toMillis(), edges: [] },
|
||||||
locations: span.locations,
|
locations: span.locations,
|
||||||
|
roles: span.roles,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,6 +332,7 @@ function* cutSpansByHours(span: Span, timezone: string): Generator<Span> {
|
||||||
start: { ts: currentStart.toMillis(), edges: [] },
|
start: { ts: currentStart.toMillis(), edges: [] },
|
||||||
end: span.end,
|
end: span.end,
|
||||||
locations: span.locations,
|
locations: span.locations,
|
||||||
|
roles: span.roles,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,12 +352,14 @@ function padStretch(stretch: Stretch, timezone: string): Stretch {
|
||||||
start: { ts: start.toMillis(), edges: [] },
|
start: { ts: start.toMillis(), edges: [] },
|
||||||
end: stretch.spans[0].start,
|
end: stretch.spans[0].start,
|
||||||
locations: new Map(),
|
locations: new Map(),
|
||||||
|
roles: new Map(),
|
||||||
},
|
},
|
||||||
...stretch.spans,
|
...stretch.spans,
|
||||||
{
|
{
|
||||||
start: stretch.spans[stretch.spans.length - 1].end,
|
start: stretch.spans[stretch.spans.length - 1].end,
|
||||||
end: { ts: end.toMillis(), edges: [] },
|
end: { ts: end.toMillis(), edges: [] },
|
||||||
locations: new Map(),
|
locations: new Map(),
|
||||||
|
roles: new Map(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
start: start.toMillis(),
|
start: start.toMillis(),
|
||||||
|
@ -314,17 +371,23 @@ function tableElementsFromStretches(
|
||||||
stretches: Iterable<Stretch>,
|
stretches: Iterable<Stretch>,
|
||||||
events: ScheduleEvent[],
|
events: ScheduleEvent[],
|
||||||
locations: ScheduleLocation[],
|
locations: ScheduleLocation[],
|
||||||
|
rota: Shift[] | undefined,
|
||||||
|
roles: Role[] | undefined,
|
||||||
timezone: string,
|
timezone: string,
|
||||||
) {
|
) {
|
||||||
type Col = { minutes?: number };
|
type Col = { minutes?: number };
|
||||||
type DayHead = { span: number, content?: string }
|
type DayHead = { span: number, content?: string }
|
||||||
type HourHead = { span: number, content?: string }
|
type HourHead = { span: number, content?: string }
|
||||||
type LocationCell = { span: number, slots: Set<TimeSlot>, title: string, crew?: boolean }
|
type LocationCell = { span: number, slots: Set<TimeSlot>, title: string, crew?: boolean }
|
||||||
|
type RoleCell = { span: number, slots: Set<ShiftSlot>, title: string };
|
||||||
const columnGroups: { className?: string, cols: Col[] }[] = [];
|
const columnGroups: { className?: string, cols: Col[] }[] = [];
|
||||||
const dayHeaders: DayHead[] = [];
|
const dayHeaders: DayHead[] = [];
|
||||||
const hourHeaders: HourHead[]= [];
|
const hourHeaders: HourHead[]= [];
|
||||||
const locationRows = new Map<string, LocationCell[]>(locations.map(location => [location.id, []]));
|
const locationRows = new Map<string, LocationCell[]>(locations.map(location => [location.id, []]));
|
||||||
|
const roleRows = new Map<string, RoleCell[]>(roles?.map?.(role => [role.id, []]));
|
||||||
const eventBySlotId = new Map(events.flatMap(event => event.slots.map(slot => [slot.id, event])));
|
const eventBySlotId = new Map(events.flatMap(event => event.slots.map(slot => [slot.id, event])));
|
||||||
|
const shiftBySlotId = new Map(rota?.flatMap?.(shift => shift.slots.map(slot =>[slot.id, shift])))
|
||||||
|
let totalColumns = 0;
|
||||||
|
|
||||||
function startColumnGroup(className?: string) {
|
function startColumnGroup(className?: string) {
|
||||||
columnGroups.push({ className, cols: []})
|
columnGroups.push({ className, cols: []})
|
||||||
|
@ -344,7 +407,16 @@ function tableElementsFromStretches(
|
||||||
}
|
}
|
||||||
rows.push({ span: 0, slots, title: "" });
|
rows.push({ span: 0, slots, title: "" });
|
||||||
}
|
}
|
||||||
|
function startRole(id: string, slots = new Set<ShiftSlot>()) {
|
||||||
|
const rows = roleRows.get(id)!;
|
||||||
|
if (rows.length) {
|
||||||
|
const row = rows[rows.length - 1];
|
||||||
|
row.title = [...row.slots].map(slot => shiftBySlotId.get(slot.id)!.name).join(", ");
|
||||||
|
}
|
||||||
|
rows.push({ span: 0, slots, title: "" });
|
||||||
|
}
|
||||||
function pushColumn(minutes?: number) {
|
function pushColumn(minutes?: number) {
|
||||||
|
totalColumns += 1;
|
||||||
columnGroups[columnGroups.length - 1].cols.push({ minutes })
|
columnGroups[columnGroups.length - 1].cols.push({ minutes })
|
||||||
dayHeaders[dayHeaders.length - 1].span += 1;
|
dayHeaders[dayHeaders.length - 1].span += 1;
|
||||||
hourHeaders[hourHeaders.length - 1].span += 1;
|
hourHeaders[hourHeaders.length - 1].span += 1;
|
||||||
|
@ -352,6 +424,10 @@ function tableElementsFromStretches(
|
||||||
const row = locationRows.get(location.id)!;
|
const row = locationRows.get(location.id)!;
|
||||||
row[row.length - 1].span += 1;
|
row[row.length - 1].span += 1;
|
||||||
}
|
}
|
||||||
|
for(const role of roles ?? []) {
|
||||||
|
const row = roleRows.get(role.id)!;
|
||||||
|
row[row.length - 1].span += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let first = true;
|
let first = true;
|
||||||
|
@ -366,6 +442,9 @@ function tableElementsFromStretches(
|
||||||
for(const location of locations) {
|
for(const location of locations) {
|
||||||
startLocation(location.id);
|
startLocation(location.id);
|
||||||
}
|
}
|
||||||
|
for(const role of roles ?? []) {
|
||||||
|
startRole(role.id);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
startColumnGroup("break");
|
startColumnGroup("break");
|
||||||
const dayName = startDate.toFormat("yyyy-LL-dd");
|
const dayName = startDate.toFormat("yyyy-LL-dd");
|
||||||
|
@ -377,6 +456,9 @@ function tableElementsFromStretches(
|
||||||
for(const location of locations) {
|
for(const location of locations) {
|
||||||
startLocation(location.id);
|
startLocation(location.id);
|
||||||
}
|
}
|
||||||
|
for(const role of roles ?? []) {
|
||||||
|
startRole(role.id);
|
||||||
|
}
|
||||||
pushColumn();
|
pushColumn();
|
||||||
|
|
||||||
startColumnGroup();
|
startColumnGroup();
|
||||||
|
@ -386,6 +468,9 @@ function tableElementsFromStretches(
|
||||||
for(const location of locations) {
|
for(const location of locations) {
|
||||||
startLocation(location.id);
|
startLocation(location.id);
|
||||||
}
|
}
|
||||||
|
for(const role of roles ?? []) {
|
||||||
|
startRole(role.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const span of stretch.spans) {
|
for (const span of stretch.spans) {
|
||||||
|
@ -401,6 +486,14 @@ function tableElementsFromStretches(
|
||||||
startLocation(location.id, slots);
|
startLocation(location.id, slots);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const role of roles ?? []) {
|
||||||
|
const rows = roleRows.get(role.id)!;
|
||||||
|
const row = rows[rows.length - 1];
|
||||||
|
const slots = cutSpan.roles.get(role.id) ?? new Set();
|
||||||
|
if (!setEquals(slots, row.slots)) {
|
||||||
|
startRole(role.id, slots);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pushColumn(durationMs / oneMinMs);
|
pushColumn(durationMs / oneMinMs);
|
||||||
const endDate = DateTime.fromMillis(end, { zone: timezone });
|
const endDate = DateTime.fromMillis(end, { zone: timezone });
|
||||||
|
@ -421,18 +514,26 @@ function tableElementsFromStretches(
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
totalColumns,
|
||||||
columnGroups,
|
columnGroups,
|
||||||
dayHeaders: dayHeaders.filter(day => day.span),
|
dayHeaders: dayHeaders.filter(day => day.span),
|
||||||
hourHeaders: hourHeaders.filter(hour => hour.span),
|
hourHeaders: hourHeaders.filter(hour => hour.span),
|
||||||
locationRows: new Map([...locationRows].map(([id, cells]) => [id, cells.filter(cell => cell.span)])),
|
locationRows: new Map([...locationRows].map(([id, cells]) => [id, cells.filter(cell => cell.span)])),
|
||||||
|
roleRows: new Map([...roleRows].map(([id, cells]) => [id, cells.filter(cell => cell.span)])),
|
||||||
eventBySlotId,
|
eventBySlotId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const schedule = await useSchedule();
|
const schedule = await useSchedule();
|
||||||
const junctions = computed(() => junctionsFromEdges(edgesFromEvents(schedule.value.events)));
|
const junctions = computed(() => junctionsFromEdges([
|
||||||
|
...edgesFromEvents(schedule.value.events),
|
||||||
|
...edgesFromShifts(schedule.value.rota ?? []),
|
||||||
|
]));
|
||||||
const stretches = computed(() => [
|
const stretches = computed(() => [
|
||||||
...stretchesFromSpans(spansFromJunctions(junctions.value, schedule.value.locations), oneHourMs * 5)
|
...stretchesFromSpans(
|
||||||
|
spansFromJunctions(junctions.value, schedule.value.locations, schedule.value.roles),
|
||||||
|
oneHourMs * 5
|
||||||
|
)
|
||||||
])
|
])
|
||||||
|
|
||||||
const runtimeConfig = useRuntimeConfig();
|
const runtimeConfig = useRuntimeConfig();
|
||||||
|
@ -444,13 +545,14 @@ const timezone = computed({
|
||||||
});
|
});
|
||||||
|
|
||||||
const elements = computed(() => tableElementsFromStretches(
|
const elements = computed(() => tableElementsFromStretches(
|
||||||
stretches.value, schedule.value.events, schedule.value.locations, timezone.value
|
stretches.value, schedule.value.events, schedule.value.locations, schedule.value.rota, schedule.value.roles, timezone.value
|
||||||
));
|
));
|
||||||
|
const totalColumns = computed(() => elements.value.totalColumns);
|
||||||
const columnGroups = computed(() => elements.value.columnGroups);
|
const columnGroups = computed(() => elements.value.columnGroups);
|
||||||
const dayHeaders = computed(() => elements.value.dayHeaders);
|
const dayHeaders = computed(() => elements.value.dayHeaders);
|
||||||
const hourHeaders = computed(() => elements.value.hourHeaders);
|
const hourHeaders = computed(() => elements.value.hourHeaders);
|
||||||
const locationRows = computed(() => elements.value.locationRows);
|
const locationRows = computed(() => elements.value.locationRows);
|
||||||
const eventBySlotId = computed(() => elements.value.eventBySlotId);
|
const roleRows = computed(() => elements.value.roleRows);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -500,7 +602,7 @@ const eventBySlotId = computed(() => elements.value.eventBySlotId);
|
||||||
background-color: color-mix(in oklab, var(--background), rgb(50, 50, 255) 60%);
|
background-color: color-mix(in oklab, var(--background), rgb(50, 50, 255) 60%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.event {
|
.event, .shift {
|
||||||
background-color: color-mix(in oklab, var(--background), rgb(255, 125, 50) 60%);
|
background-color: color-mix(in oklab, var(--background), rgb(255, 125, 50) 60%);
|
||||||
}
|
}
|
||||||
.event.crew {
|
.event.crew {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Account } from "~/shared/types/account";
|
import { Account } from "~/shared/types/account";
|
||||||
import { Schedule, TimeSlot } from "~/shared/types/schedule";
|
import { Role, Schedule, Shift, ShiftSlot, TimeSlot } from "~/shared/types/schedule";
|
||||||
|
|
||||||
const locations = [
|
const locations = [
|
||||||
{
|
{
|
||||||
|
@ -60,8 +60,8 @@ const events = [
|
||||||
},
|
},
|
||||||
{ name: "Fishing Trip", slots: ["d3 12:00 3h30m outside"]},
|
{ name: "Fishing Trip", slots: ["d3 12:00 3h30m outside"]},
|
||||||
{ name: "Opening", slots: ["d1 18:30 1h30m stage"]},
|
{ name: "Opening", slots: ["d1 18:30 1h30m stage"]},
|
||||||
{ name: "Closing", slots: ["d5 16:00 1h stage"]},
|
{ name: "Closing", slots: ["d5 10:00 1h stage"]},
|
||||||
{ name: "Stage Teardown", crew: true, slots: ["d5 17:00 4h stage"]},
|
{ name: "Stage Teardown", crew: true, slots: ["d5 11:00 4h stage"]},
|
||||||
{ name: "Setup Board Games", crew: true, slots: ["d1 11:30 30m summerhouse"]},
|
{ name: "Setup Board Games", crew: true, slots: ["d1 11:30 30m summerhouse"]},
|
||||||
{
|
{
|
||||||
name: "Board Games",
|
name: "Board Games",
|
||||||
|
@ -131,9 +131,59 @@ const events = [
|
||||||
{ name: "Setup Artist Alley", crew: true, slots: ["d4 10:00 2h clubhouse"]},
|
{ name: "Setup Artist Alley", crew: true, slots: ["d4 10:00 2h clubhouse"]},
|
||||||
{ name: "Artist Alley", slots: ["d4 12:00 4h clubhouse"]},
|
{ name: "Artist Alley", slots: ["d4 12:00 4h clubhouse"]},
|
||||||
{ name: "Teardown Artist Alley", crew: true, slots: ["d4 16:00 1h clubhouse"]},
|
{ name: "Teardown Artist Alley", crew: true, slots: ["d4 16:00 1h clubhouse"]},
|
||||||
{ name: "Feedback Panel", slots: ["d5 18:00 1h clubhouse"]},
|
{ name: "Feedback Panel", slots: ["d5 12:00 1h clubhouse"]},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const roles: Role[] = [
|
||||||
|
{ id: "medic", name: "Medic" },
|
||||||
|
{ id: "security", name: "Security" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const rota = [
|
||||||
|
{
|
||||||
|
name: "Medic Early",
|
||||||
|
role: "medic",
|
||||||
|
slots: [
|
||||||
|
"d1 12:00 4h",
|
||||||
|
"d2 12:00 4h",
|
||||||
|
"d3 12:00 4h",
|
||||||
|
"d4 11:00 5h",
|
||||||
|
"d5 10:00 3h",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Medic Late",
|
||||||
|
role: "medic",
|
||||||
|
slots: [
|
||||||
|
"d1 16:00 7h",
|
||||||
|
"d2 16:00 6h",
|
||||||
|
"d3 16:00 8h",
|
||||||
|
"d4 16:00 7h",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Security Early",
|
||||||
|
role: "security",
|
||||||
|
slots: [
|
||||||
|
"d1 12:00 6h",
|
||||||
|
"d2 12:00 6h",
|
||||||
|
"d3 12:00 6h",
|
||||||
|
"d4 11:00 7h",
|
||||||
|
"d5 10:00 3h",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Security Late",
|
||||||
|
role: "security",
|
||||||
|
slots: [
|
||||||
|
"d1 18:00 5h",
|
||||||
|
"d2 18:00 4h",
|
||||||
|
"d3 18:00 6h",
|
||||||
|
"d4 18:00 5h",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
function toId(name: string) {
|
function toId(name: string) {
|
||||||
return name.toLowerCase().replace(/ /g, "-");
|
return name.toLowerCase().replace(/ /g, "-");
|
||||||
}
|
}
|
||||||
|
@ -142,8 +192,7 @@ function toIso(date: Date) {
|
||||||
return date.toISOString().replace(":00.000Z", "Z");
|
return date.toISOString().replace(":00.000Z", "Z");
|
||||||
}
|
}
|
||||||
|
|
||||||
function toSlot(origin: Date, id: string, shorthand: string, index: number, counts: Map<string, number>): TimeSlot {
|
function toDates(origin: Date, day: string, start: string, duration: string) {
|
||||||
const [day, start, duration, location] = shorthand.split(" ");
|
|
||||||
const [startHours, startMinutes] = start.split(":").map(time => parseInt(time, 10));
|
const [startHours, startMinutes] = start.split(":").map(time => parseInt(time, 10));
|
||||||
const dayNumber = parseInt(day.slice(1));
|
const dayNumber = parseInt(day.slice(1));
|
||||||
|
|
||||||
|
@ -156,6 +205,13 @@ function toSlot(origin: Date, id: string, shorthand: string, index: number, coun
|
||||||
const durationTotal = parseInt(durationHours ?? "0") * 60 + parseInt(durationMinutes ?? "0")
|
const durationTotal = parseInt(durationHours ?? "0") * 60 + parseInt(durationMinutes ?? "0")
|
||||||
const endDate = new Date(startDate.getTime() + durationTotal * 60e3);
|
const endDate = new Date(startDate.getTime() + durationTotal * 60e3);
|
||||||
|
|
||||||
|
return [startDate, endDate];
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSlot(origin: Date, id: string, shorthand: string, index: number, counts: Map<string, number>): TimeSlot {
|
||||||
|
const [day, start, duration, location] = shorthand.split(" ");
|
||||||
|
const [startDate, endDate] = toDates(origin, day, start, duration);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `${id}-${index}`,
|
id: `${id}-${index}`,
|
||||||
start: toIso(startDate),
|
start: toIso(startDate),
|
||||||
|
@ -165,6 +221,17 @@ function toSlot(origin: Date, id: string, shorthand: string, index: number, coun
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toShift(origin: Date, id: string, shorthand: string, index: number): ShiftSlot {
|
||||||
|
const [day, start, duration] = shorthand.split(" ");
|
||||||
|
const [startDate, endDate] = toDates(origin, day, start, duration);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `${id}-${index}`,
|
||||||
|
start: toIso(startDate),
|
||||||
|
end: toIso(endDate),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function generateDemoSchedule(): Schedule {
|
export function generateDemoSchedule(): Schedule {
|
||||||
const origin = new Date();
|
const origin = new Date();
|
||||||
const utcOffset = 1;
|
const utcOffset = 1;
|
||||||
|
@ -195,6 +262,15 @@ export function generateDemoSchedule(): Schedule {
|
||||||
locations: locations.map(
|
locations: locations.map(
|
||||||
({ name, description }) => ({ id: toId(name), name, description })
|
({ name, description }) => ({ id: toId(name), name, description })
|
||||||
),
|
),
|
||||||
|
roles,
|
||||||
|
rota: rota.map(
|
||||||
|
({ name, role, slots }) => ({
|
||||||
|
id: toId(name),
|
||||||
|
name,
|
||||||
|
role,
|
||||||
|
slots: slots.map((shorthand, index) => toShift(origin, toId(name), shorthand, index))
|
||||||
|
})
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
22
shared/types/schedule.d.ts
vendored
22
shared/types/schedule.d.ts
vendored
|
@ -23,7 +23,29 @@ export interface TimeSlot {
|
||||||
interested?: number,
|
interested?: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Shift {
|
||||||
|
name: string,
|
||||||
|
id: string,
|
||||||
|
role: string,
|
||||||
|
description?: string,
|
||||||
|
slots: ShiftSlot[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Role {
|
||||||
|
name: string,
|
||||||
|
id: string,
|
||||||
|
description?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShiftSlot {
|
||||||
|
id: string,
|
||||||
|
start: string,
|
||||||
|
end: string,
|
||||||
|
}
|
||||||
|
|
||||||
export interface Schedule {
|
export interface Schedule {
|
||||||
locations: ScheduleLocation[],
|
locations: ScheduleLocation[],
|
||||||
events: ScheduleEvent[],
|
events: ScheduleEvent[],
|
||||||
|
roles?: Role[],
|
||||||
|
rota?: Shift[],
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue