"Enhance DepartmentDashboardOrderBooking.vue with item management functionality, including add, update, and delete item actions, dynamic total calculation, and improved error handling; refine UI with department name display and detailed item list."

This commit is contained in:
Jeppe Bundgaard
2025-11-10 11:19:21 +01:00
parent c627d71f73
commit 66415447c7
@@ -1,43 +1,221 @@
<script setup lang="ts">
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { ref, watch } from 'vue';
import { useRouter} from "vue-router";
import { ref, watch, computed, onMounted } from 'vue';
import { useRouter } from "vue-router";
import Swal from "sweetalert2";
/**
* Parameters & variables
*/
const bookingId = ref(null);
const booking = ref(null);
/**
* Initialize the router
*/
// Route params
const router = useRouter();
bookingId.value = router.currentRoute.value.params.bookingId;
const bookingId = ref<any>(router.currentRoute.value.params.bookingId || null);
/**
* Get the booking
*/
const getBooking = async () => {
if (!bookingId.value) {
return;
// State
const loading = ref(false);
const saving = ref(false);
const error = ref<string | null>(null);
const booking = ref<any>(null);
// Add item form state
const products = ref<any[]>([]);
const selectedProductId = ref<number | null>(null);
const selectedQuantity = ref<number>(1);
// Fetch products once (used for add form)
const loadProducts = async () => {
try {
const all = await SessionUser.objects.products.get.all();
// If department filter is needed, backend typically handles via get.category; for now list all
products.value = all || [];
} catch (e) {
console.warn('Failed to load products', e);
}
await SessionUser.objects.order_bookings.get.single(bookingId.value).then((response) => {
booking.value = response;
});
}
watch(bookingId, () => {
console.warn("Booking ID changed, fetching booking...", bookingId.value);
getBooking();
})
getBooking();
// Helpers
const departmentName = computed(() => {
if (!booking.value) return '';
// Best-effort: show department id; elsewhere there's a getDepartmentName util in user table
return `Afdeling #${booking.value.department}`;
});
// Load booking
const getBooking = async () => {
if (!bookingId.value) return;
loading.value = true;
error.value = null;
try {
booking.value = await SessionUser.objects.order_bookings.get.single(bookingId.value);
} catch (e: any) {
console.error(e);
error.value = 'Kunne ikke indlæse booking.';
} finally {
loading.value = false;
}
};
// Mutations: persist items array
const persistItems = async (items: any[]) => {
if (!booking.value) return;
saving.value = true;
try {
await SessionUser.objects.order_bookings.set.items(booking.value.id, items);
await getBooking();
Swal.fire({ icon: 'success', title: 'Opdateret', text: 'Ydelser opdateret', timer: 1200, showConfirmButton: false });
} catch (e) {
console.error('Failed to update items', e);
Swal.fire({ icon: 'error', title: 'Fejl', text: 'Kunne ikke opdatere ydelser' });
} finally {
saving.value = false;
}
};
// Actions
const onRemoveItem = async (index: number) => {
if (!booking.value) return;
const newItems = [...(booking.value.items || [])];
newItems.splice(index, 1);
await persistItems(newItems);
};
const onChangeQty = async (index: number, delta: number) => {
if (!booking.value) return;
const newItems = [...(booking.value.items || [])];
const item = { ...(newItems[index] || {}) };
const qty = Math.max(1, parseInt(item.quantity || 1) + delta);
item.quantity = qty;
newItems[index] = item;
await persistItems(newItems);
};
const onAddItem = async () => {
if (!booking.value || !selectedProductId.value) return;
// Try to fetch product details to include name/price if backend expects it
try {
const product = await SessionUser.objects.products.get.single(parseInt(String(selectedProductId.value)));
const newItem = {
// Keep keys similar to existing items shape seen elsewhere (name, price, is_wash, id as product id)
id: product.id,
product_id: product.id,
name: product.name,
price: product.price,
is_wash: !!product.is_wash,
quantity: selectedQuantity.value || 1,
};
const newItems = [ ...((booking.value.items as any[]) || []), newItem ];
await persistItems(newItems);
// Reset form
selectedProductId.value = null;
selectedQuantity.value = 1;
} catch (e) {
console.error('Failed to add product', e);
Swal.fire({ icon: 'error', title: 'Fejl', text: 'Kunne ikke tilføje ydelse' });
}
};
// Derived
const items = computed<any[]>(() => booking.value?.items || []);
const total = computed(() => {
return (items.value || []).reduce((sum, it: any) => sum + (parseFloat(it.price || 0) * (parseInt(it.quantity || 1))), 0);
});
watch(() => router.currentRoute.value.params.bookingId, (val) => {
bookingId.value = val as any;
getBooking();
});
onMounted(async () => {
await Promise.all([getBooking(), loadProducts()]);
});
</script>
<template>
<div>
<div class="mb-4">
<h1 class="title is-4">Booking #{{ booking?.id || bookingId }}</h1>
<p class="subtitle is-6" v-if="booking">
{{ departmentName }} {{ booking.datetime }} Køretøjer: {{ booking.reg_1 }}<span v-if="booking.reg_2">, {{ booking.reg_2 }}</span>
</p>
<div class="tags" v-if="booking">
<span class="tag is-info" v-if="booking.po">PO: {{ booking.po }}</span>
<span class="tag is-light" v-if="booking.reference">Ref: {{ booking.reference }}</span>
<span class="tag is-warning" v-if="booking.pickup">Pickup</span>
</div>
</div>
<div v-if="loading" class="notification is-light">Indlæser booking...</div>
<div v-if="error" class="notification is-danger">{{ error }}</div>
<div v-if="booking">
<!-- Items list -->
<div class="box">
<div class="is-flex is-justify-content-space-between is-align-items-center mb-3">
<h2 class="title is-5 mb-0">Ydelser</h2>
<span class="has-text-weight-semibold">Total: {{ total.toLocaleString('da-DK', { style: 'currency', currency: 'DKK' }) }}</span>
</div>
<table class="table is-fullwidth is-striped is-hoverable">
<thead>
<tr>
<th>Navn</th>
<th class="has-text-centered">Antal</th>
<th class="has-text-right">Pris</th>
<th style="width:130px;" class="has-text-right">Handlinger</th>
</tr>
</thead>
<tbody>
<tr v-if="items.length === 0">
<td colspan="4" class="has-text-centered has-text-grey">Ingen ydelser tilføjet endnu</td>
</tr>
<tr v-for="(item, index) in items" :key="index">
<td>
<span class="has-text-weight-medium">{{ item.name || ('Produkt #' + (item.id || item.product_id)) }}</span>
<span class="tag is-light is-rounded ml-2" v-if="item.is_wash">Vask</span>
</td>
<td class="has-text-centered">
<div class="buttons are-small is-centered">
<button class="button" :disabled="saving" @click="onChangeQty(index, -1)"><i class="fas fa-minus"></i></button>
<span class="button is-static">{{ item.quantity || 1 }}</span>
<button class="button" :disabled="saving" @click="onChangeQty(index, +1)"><i class="fas fa-plus"></i></button>
</div>
</td>
<td class="has-text-right">{{ (parseFloat(item.price || 0) * (parseInt(item.quantity || 1))).toLocaleString('da-DK', { style: 'currency', currency: 'DKK' }) }}</td>
<td class="has-text-right">
<button class="button is-small is-danger" :disabled="saving" @click="onRemoveItem(index)">
<span class="icon"><i class="fas fa-trash"></i></span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Add item form -->
<div class="box">
<h2 class="title is-6">Tilføj ydelse</h2>
<div class="field is-grouped">
<div class="control is-expanded">
<div class="select is-fullwidth">
<select v-model.number="selectedProductId">
<option :value="null">Vælg produkt</option>
<option v-for="p in products" :key="p.id" :value="p.id">{{ p.name }} {{ p.price?.toLocaleString('da-DK', { style: 'currency', currency: 'DKK' }) }}</option>
</select>
</div>
</div>
<div class="control">
<input class="input" type="number" min="1" v-model.number="selectedQuantity" style="width:100px" />
</div>
<div class="control">
<button class="button is-primary" :class="{ 'is-loading': saving }" :disabled="!selectedProductId || selectedQuantity < 1" @click="onAddItem">
Tilføj
</button>
</div>
</div>
</div>
<pre class="mt-4" style="white-space: pre-wrap;">{{ booking }}</pre>
</div>
</div>
</template>
<style scoped>
.buttons.is-centered { justify-content: center; }
</style>