queue today assignments by schedule

This commit is contained in:
Yisroel Baum 2026-08-18 21:07:43 +03:00
parent ba6e4908da
commit fea6c3f7a8
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9

View file

@ -1,17 +1,40 @@
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import AuthenticatedHeader from '@/components/AuthenticatedHeader.vue' import AuthenticatedHeader from '@/components/AuthenticatedHeader.vue'
import { useSchedulesStore } from '@/stores/schedules' import { type AssignmentForDate, useSchedulesStore } from '@/stores/schedules'
import { useSetsStore } from '@/stores/sets' import { useSetsStore } from '@/stores/sets'
type TodayAssignmentQueue = {
assignment: AssignmentForDate
dueCount: number
}
const setsStore = useSetsStore() const setsStore = useSetsStore()
const schedulesStore = useSchedulesStore() const schedulesStore = useSchedulesStore()
const { sets, loading, error } = storeToRefs(setsStore) const { sets, loading, error } = storeToRefs(setsStore)
const { schedules, listLoading, listError } = storeToRefs(schedulesStore) const { schedules, listLoading, listError } = storeToRefs(schedulesStore)
const { assignmentsForDate, assignmentsLoading, assignmentsError } = storeToRefs(schedulesStore) const { assignmentsForDate, assignmentsLoading, assignmentsError } = storeToRefs(schedulesStore)
const todayDate = ref(browserDate(new Date())) const todayDate = ref(browserDate(new Date()))
const todayAssignmentQueues = computed<TodayAssignmentQueue[]>(() => {
const queuesBySchedule = new Map<number, TodayAssignmentQueue>()
for (const assignment of assignmentsForDate.value) {
const queue = queuesBySchedule.get(assignment.schedule.id)
if (queue !== undefined) {
queue.dueCount += 1
continue
}
queuesBySchedule.set(assignment.schedule.id, {
assignment,
dueCount: 1,
})
}
return Array.from(queuesBySchedule.values())
})
onMounted(async () => { onMounted(async () => {
await Promise.all([ await Promise.all([
@ -83,36 +106,54 @@ async function completeAssignment(assignmentId: number): Promise<void> {
</button> </button>
</div> </div>
<p v-else-if="assignmentsForDate.length === 0" class="today-state" role="status"> <p v-else-if="todayAssignmentQueues.length === 0" class="today-state" role="status">
Nothing is due today. Nothing is due today.
</p> </p>
<ul v-else class="today-assignment-list" aria-label="Today's assignments"> <ul v-else class="today-assignment-list" aria-label="Today's assignments">
<li <li
v-for="assignment in assignmentsForDate" v-for="queue in todayAssignmentQueues"
:key="assignment.id" :key="queue.assignment.schedule.id"
:data-today-assignment="assignment.id" class="today-assignment-stack"
:class="{ 'today-assignment-stack--queued': queue.dueCount > 1 }"
:data-due-count="queue.dueCount"
:data-today-assignment="queue.assignment.id"
:data-today-schedule="queue.assignment.schedule.id"
> >
<article class="today-assignment"> <article class="today-assignment">
<RouterLink <RouterLink
class="today-assignment-link" class="today-assignment-link"
:to="{ :to="{
name: 'schedule-detail', name: 'schedule-detail',
params: { scheduleId: assignment.schedule.id }, params: { scheduleId: queue.assignment.schedule.id },
}" }"
> >
<div class="today-assignment__heading"> <div class="today-assignment__heading">
<p>{{ assignment.schedule.set.name }}</p> <p>{{ queue.assignment.schedule.set.name }}</p>
<span class="today-assignment__kind">{{ assignment.element.kind }}</span> <span class="today-assignment__kind">
{{ queue.assignment.element.kind }}
</span>
</div>
<div
v-if="isOverdue(queue.assignment.scheduledDate) || queue.dueCount > 1"
class="today-assignment__status"
>
<time
v-if="isOverdue(queue.assignment.scheduledDate)"
class="today-assignment__overdue"
:datetime="queue.assignment.scheduledDate"
v-text="overdueLabel(queue.assignment.scheduledDate)"
></time>
<span
v-if="queue.dueCount > 1"
class="today-assignment__queue-count"
:aria-label="`${queue.dueCount} assignments from this schedule are due now. Complete this assignment first.`"
>
{{ queue.dueCount }} due now
</span>
</div> </div>
<time
v-if="isOverdue(assignment.scheduledDate)"
class="today-assignment__overdue"
:datetime="assignment.scheduledDate"
v-text="overdueLabel(assignment.scheduledDate)"
></time>
<p class="today-assignment__path"> <p class="today-assignment__path">
{{ assignment.element.path.join(' / ') }} {{ queue.assignment.element.path.join(' / ') }}
</p> </p>
<span class="today-assignment__action"> <span class="today-assignment__action">
Open schedule Open schedule
@ -121,21 +162,21 @@ async function completeAssignment(assignmentId: number): Promise<void> {
</RouterLink> </RouterLink>
<div class="today-assignment__completion"> <div class="today-assignment__completion">
<p <p
v-if="schedulesStore.assignmentCompletionError(assignment.id) !== null" v-if="schedulesStore.assignmentCompletionError(queue.assignment.id) !== null"
class="assignment-completion-error" class="assignment-completion-error"
role="alert" role="alert"
> >
{{ schedulesStore.assignmentCompletionError(assignment.id) }} {{ schedulesStore.assignmentCompletionError(queue.assignment.id) }}
</p> </p>
<button <button
type="button" type="button"
class="assignment-completion-button" class="assignment-completion-button"
:aria-label="`Mark ${assignment.element.path.join(' / ')} complete`" :aria-label="`Mark ${queue.assignment.element.path.join(' / ')} complete`"
:disabled="schedulesStore.isAssignmentCompletionPending(assignment.id)" :disabled="schedulesStore.isAssignmentCompletionPending(queue.assignment.id)"
@click="completeAssignment(assignment.id)" @click="completeAssignment(queue.assignment.id)"
> >
{{ {{
schedulesStore.isAssignmentCompletionPending(assignment.id) schedulesStore.isAssignmentCompletionPending(queue.assignment.id)
? 'Completing...' ? 'Completing...'
: 'Mark complete' : 'Mark complete'
}} }}
@ -355,6 +396,40 @@ async function completeAssignment(assignmentId: number): Promise<void> {
list-style: none; list-style: none;
} }
.today-assignment-stack {
position: relative;
display: flex;
min-width: 0;
}
.today-assignment-stack--queued {
padding: 0 0.55rem 0.55rem 0;
}
.today-assignment-stack--queued::before,
.today-assignment-stack--queued::after {
position: absolute;
pointer-events: none;
content: '';
}
.today-assignment-stack--queued::before {
z-index: 1;
inset: 0.28rem 0.28rem 0.28rem 0.28rem;
border: 1px solid rgb(24 48 41 / 10%);
border-radius: 1rem;
background: #eee9dc;
transform: translate(0.28rem, 0.28rem);
}
.today-assignment-stack--queued::after {
z-index: 0;
inset: 0.55rem 0 0 0.55rem;
border: 1px solid rgb(24 48 41 / 8%);
border-radius: 1rem;
background: #e5dfd1;
}
.today-assignment-link { .today-assignment-link {
display: block; display: block;
color: inherit; color: inherit;
@ -362,8 +437,11 @@ async function completeAssignment(assignmentId: number): Promise<void> {
} }
.today-assignment { .today-assignment {
position: relative;
z-index: 2;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%;
height: 100%; height: 100%;
padding: 1.4rem; padding: 1.4rem;
border: 1px solid rgb(24 48 41 / 12%); border: 1px solid rgb(24 48 41 / 12%);
@ -404,19 +482,35 @@ async function completeAssignment(assignmentId: number): Promise<void> {
text-transform: none; text-transform: none;
} }
.today-assignment__overdue { .today-assignment__status {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin-top: 0.85rem;
}
.today-assignment__overdue,
.today-assignment__queue-count {
display: inline-flex; display: inline-flex;
width: fit-content; width: fit-content;
margin-top: 0.85rem;
padding: 0.3rem 0.55rem; padding: 0.3rem 0.55rem;
border-radius: 999px; border-radius: 999px;
color: #923f31;
background: #f7e5dc;
font-size: 0.68rem; font-size: 0.68rem;
font-weight: 800; font-weight: 800;
letter-spacing: 0.025em; letter-spacing: 0.025em;
} }
.today-assignment__overdue {
color: #923f31;
background: #f7e5dc;
}
.today-assignment__queue-count {
color: #285c4e;
background: #e2ebe4;
}
.today-assignment__path { .today-assignment__path {
margin: 1.75rem 0 0; margin: 1.75rem 0 0;
color: #183029; color: #183029;