cargagep-v2/frontend/src/components/admin/ReservationCard.vue
2026-08-24 13:02:01 +02:00

99 lines
3.5 KiB
Vue

<script setup lang="ts">
/**
* One reservation, with the transitions its current status allows. The buttons
* mirror `ReservationStatus::can_transition_to` on the backend, which re-checks
* and answers 409 on anything else.
*/
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { Pencil } from '@lucide/vue'
import { toast } from 'vue-sonner'
import ReservationDetails from '@/components/reservation/ReservationDetails.vue'
import ReservationEditDialog from '@/components/reservation/ReservationEditDialog.vue'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { useSetReservationStatus } from '@/services/api/reservations'
import { unitLabel, type Bike, type Reservation, type ReservationStatus } from '@/utils/types'
const props = defineProps<{ reservation: Reservation; bikes: Bike[] }>()
const { t } = useI18n()
const setStatus = useSetReservationStatus()
/** Kept in step with the state machine drawn on the backend enum */
const TRANSITIONS: Record<ReservationStatus, ReservationStatus[]> = {
requested: ['approved', 'refused'],
// No "start" here on purpose: a reservation becomes `ongoing` on its own, not
// by an admin pressing a button. The backend still allows the transition.
approved: ['cancelled'],
ongoing: ['archived', 'cancelled'],
refused: [],
cancelled: [],
archived: [],
}
const BADGE_CLASS: Record<ReservationStatus, string> = {
requested: 'bg-amber-500/15 text-amber-700 dark:text-amber-400',
approved: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400',
ongoing: 'bg-primary/15 text-primary',
refused: 'bg-destructive/15 text-destructive',
cancelled: 'bg-destructive/15 text-destructive',
archived: 'bg-muted text-muted-foreground',
}
const transitions = computed(() => TRANSITIONS[props.reservation.status])
/** A reservation nobody can act on any more is not worth an edit button */
const editable = computed(
() => !['refused', 'cancelled', 'archived'].includes(props.reservation.status),
)
const editing = ref(false)
function move(status: ReservationStatus) {
setStatus.mutate(
{ id: props.reservation.id, status },
{ onError: () => toast.error(t('admin.reservations.error')) },
)
}
</script>
<template>
<div class="rounded-lg border p-4">
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="flex min-w-0 flex-wrap items-center gap-2">
<span class="font-semibold">#{{ reservation.id }}</span>
<Badge variant="secondary" :class="BADGE_CLASS[reservation.status]">
{{ $t(`reservation.status.${reservation.status}`) }}
</Badge>
<span class="text-muted-foreground text-sm">{{ unitLabel(reservation.unit) }}</span>
</div>
<div v-if="transitions.length || editable" class="flex flex-wrap gap-2">
<Button v-if="editable" size="sm" variant="outline" @click="editing = true">
<Pencil class="size-4" />
{{ $t('reservation.edit.action') }}
</Button>
<Button
v-for="status in transitions"
:key="status"
size="sm"
:variant="status === 'approved' ? 'default' : 'outline'"
:disabled="setStatus.isPending.value"
@click="move(status)"
>
{{ $t(`admin.reservations.action.${status}`) }}
</Button>
</div>
</div>
<ReservationDetails class="mt-3" :reservation="reservation" :bikes="bikes" />
<ReservationEditDialog
v-if="editable"
v-model:open="editing"
:reservation="reservation"
:bikes="bikes"
/>
</div>
</template>