Introduced Department POS functionality with navigation, orders, and order detail views. Set up routing and middleware for POS operations. Added base styles, assets, and updated package dependencies to support these changes.
397 lines
13 KiB
Vue
397 lines
13 KiB
Vue
<script setup>
|
|
defineProps(['objects', 'meta']);
|
|
import { ref, watch } from 'vue';
|
|
import { departments, getDepartments, isLoading, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
|
import { showDownloadWashCertificate } from "@/components/shop/DownloadWashCertificate.vue";
|
|
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
|
import Swal from 'sweetalert2';
|
|
const redirectBookingObjectPage = (objectId) => {
|
|
// Send the user to the object page
|
|
window.location.href = `/user/bookings/${objectId}`;
|
|
};
|
|
|
|
const openUrlInNewTab = (url) => {
|
|
// Open the URL in a new tab
|
|
window.open(url, '_blank');
|
|
};
|
|
|
|
const deleteBooking = (object) => {
|
|
// Create a delete booking request
|
|
authenticatedRequest('/admin/bookings/delete', 'post', {
|
|
id: object.id
|
|
}).then(
|
|
(response) => {
|
|
// Create a success alert
|
|
Swal.fire({
|
|
title: 'Success',
|
|
html: 'The booking has been deleted',
|
|
icon: 'success',
|
|
confirmButtonText: 'Close',
|
|
timer: 5000
|
|
});
|
|
// Change the booking status to cancelled in the bookings array
|
|
object.status = 'cancelled';
|
|
}
|
|
).catch((error) => {
|
|
// Create an error alert
|
|
Swal.fire({
|
|
title: 'Error',
|
|
html: '' + error.response.data.data.message,
|
|
icon: 'error',
|
|
confirmButtonText: 'Close'
|
|
});
|
|
});
|
|
};
|
|
|
|
const deleteBookingUser = (object) => {
|
|
// Create a delete booking request
|
|
authenticatedRequest('/user/bookings/delete', 'post', {
|
|
id: object.id
|
|
}).then(
|
|
(response) => {
|
|
// Create a success alert
|
|
Swal.fire({
|
|
title: 'Success',
|
|
html: 'Bookingen er blevet annulleret',
|
|
icon: 'success',
|
|
confirmButtonText: 'Luk',
|
|
timer: 5000
|
|
});
|
|
// Change the booking status to cancelled in the bookings array
|
|
object.status = 'cancelled';
|
|
}
|
|
).catch((error) => {
|
|
// Create an error alert
|
|
Swal.fire({
|
|
title: 'Error',
|
|
html: '' + error.response.data.data.message,
|
|
icon: 'error',
|
|
confirmButtonText: 'Close'
|
|
});
|
|
});
|
|
};
|
|
|
|
|
|
const showDeleteBooking = (object) => {
|
|
// Show a delete booking alert
|
|
Swal.fire({
|
|
title: 'Annuller booking',
|
|
html: 'Er du sikker på at du vil annullere bookingen?',
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Annuller booking',
|
|
cancelButtonText: 'Fortryd',
|
|
allowOutsideClick: false,
|
|
allowEscapeKey: false,
|
|
allowEnterKey: false
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
// Delete the booking
|
|
if (SessionUser.canAccessAdmin() === true) {
|
|
deleteBooking(object);
|
|
} else {
|
|
deleteBookingUser(object);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
const showCompleteWashWithoutWashCertificate = (object) => {
|
|
// Show a complete wash without wash certificate alert
|
|
Swal.fire({
|
|
title: 'Gennemfør vask uden vaskecertifikat',
|
|
html: 'Skriv booking ID\'et for at gennemføre vasken uden vaskecertifikat',
|
|
icon: 'warning',
|
|
input: 'text',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Yes',
|
|
cancelButtonText: 'No',
|
|
allowOutsideClick: false,
|
|
allowEscapeKey: false,
|
|
allowEnterKey: false,
|
|
inputValidator: (value) => {
|
|
if (!value) {
|
|
return 'Du skal skrive booking ID\'et for at gennemføre vasken uden vaskecertifikat';
|
|
}
|
|
if (object.id !== parseInt(value)) {
|
|
return 'Booking ID\'et er forkert';
|
|
}
|
|
}
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
// Complete the wash without wash certificate
|
|
completeWashWithoutWashCertificate(object);
|
|
}
|
|
});
|
|
};
|
|
|
|
const completeWashWithoutWashCertificate = (object) => {
|
|
// Create a complete wash without wash certificate request
|
|
authenticatedRequest('/admin/bookings/completeWashWithoutWashCertificate', 'post', {
|
|
id: object.id
|
|
}).then(
|
|
(response) => {
|
|
// Create a success alert
|
|
Swal.fire({
|
|
title: 'Success',
|
|
html: 'The wash has been completed',
|
|
icon: 'success',
|
|
confirmButtonText: 'Close',
|
|
timer: 5000
|
|
});
|
|
// Change the booking status to completed in the bookings array
|
|
object.status = 'completed';
|
|
}
|
|
).catch((error) => {
|
|
// Create an error alert
|
|
Swal.fire({
|
|
title: 'Error',
|
|
html: '' + error.response.data.data.message,
|
|
icon: 'error',
|
|
confirmButtonText: 'Close'
|
|
});
|
|
});
|
|
};
|
|
|
|
|
|
|
|
|
|
// Get the departments (If the departments are not already loaded)
|
|
if (departments.value.length === 0) {
|
|
getDepartments();
|
|
}
|
|
|
|
const renderCustomerName = (object) => {
|
|
if (object.customer_name) {
|
|
return object.customer_name;
|
|
}
|
|
return 'N/A';
|
|
};
|
|
|
|
|
|
// If the screen is mobile, set the is small variable to true
|
|
const isSmall = ref(window.innerWidth < 1024);
|
|
|
|
// When the window is resized, check if the screen is mobile
|
|
window.addEventListener('resize', () => {
|
|
isSmall.value = window.innerWidth < 1024;
|
|
});
|
|
|
|
/**
|
|
* To organize the bookings, we want to show the bookings that are created today first, then yesterday, then all other days
|
|
* @type {string}
|
|
*/
|
|
const currentDate = new Date().toISOString().split("T")[0];
|
|
|
|
/**
|
|
* Sort the bookings by date
|
|
* @param a
|
|
* @param b
|
|
* @returns {number}
|
|
*/
|
|
const sortBookingsByDate = (a, b) => {
|
|
if (a.date === currentDate && b.date !== currentDate) {
|
|
return -1;
|
|
}
|
|
if (a.date !== currentDate && b.date === currentDate) {
|
|
return 1;
|
|
}
|
|
if (a.date < b.date) {
|
|
return 1;
|
|
}
|
|
if (a.date > b.date) {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
/**
|
|
* Sort the bookings by date and return the sorted bookings with label rows for the date changes
|
|
* @param bookings
|
|
* @returns {*}
|
|
*/
|
|
const parseAndSortBookings = (bookings) => {
|
|
// Sort the bookings by date
|
|
bookings.sort(sortBookingsByDate);
|
|
// Create a new array for the bookings
|
|
let newBookings = [];
|
|
// Create a variable for the last date
|
|
let lastDate = null;
|
|
// Loop through the bookings
|
|
for (let i = 0; i < bookings.length; i++) {
|
|
// If the last date is not the same as the current date, add a label row
|
|
if (lastDate !== bookings[i].date) {
|
|
newBookings.push({
|
|
id: 'label-' + i,
|
|
date: bookings[i].date,
|
|
isLabel: true
|
|
});
|
|
lastDate = bookings[i].date;
|
|
}
|
|
// Add the booking to the new bookings array
|
|
newBookings.push(bookings[i]);
|
|
}
|
|
// Return the new bookings array
|
|
return newBookings;
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<table class="table is-fullwidth is-hoverable is-narrow" :class="isSmall ? 'is-hidden' : ''">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Afdeling</th>
|
|
<th>Kunde navn</th>
|
|
<th>Kunde nummer</th>
|
|
<th>Dato</th>
|
|
<th>Reg. Trækker</th>
|
|
<th>Reg. Trailer</th>
|
|
<th>Reference</th>
|
|
<th>Noter</th>
|
|
<th>Skal hentes?</th>
|
|
<th>Oprettet</th>
|
|
<th>Vaskecertifikat</th>
|
|
<th>Handlinger</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="object in parseAndSortBookings(objects)" :key="object.id">
|
|
<template v-if="object.isLabel">
|
|
<td colspan="13" class="has-text-left">
|
|
<span class="has-text-grey">
|
|
<i class="fas fa-calendar-day"></i>
|
|
</span>
|
|
<span class="has-text-grey">
|
|
<!-- Human readable date (Today, Yesterday, etc.) -->
|
|
{{ object.date === currentDate ? 'I dag' : object.date === new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().split("T")[0] ? 'I går' : object.date }}
|
|
<!-- Number of bookings on the date -->
|
|
({{ objects.filter((booking) => booking.date === object.date).length }} bookinger)
|
|
</span>
|
|
</td>
|
|
</template>
|
|
<template v-else>
|
|
<td>{{ object.id }}</td>
|
|
<td>{{ getDepartmentName(object.department) }}</td>
|
|
<td>{{ renderCustomerName(object) }}</td>
|
|
<td>{{ object.customer_number }}</td>
|
|
<td>{{ object.date }}</td>
|
|
<td>{{ object.regNrTraekker }}</td>
|
|
<td>{{ object.regNrTrailer }}</td>
|
|
<td>{{ object.reference_number }}</td>
|
|
<td>{{ object.notes }}</td>
|
|
<td>{{ object.pickup_bool == 1 ? "Ja" : "Nej" }}</td>
|
|
<td>{{ object.created_at }}</td>
|
|
<td>
|
|
<div class="buttons">
|
|
<!-- If the washCertificateUrl is not empty, show the button to download the wash certificate -->
|
|
<button v-if="object.status === 'completed' && object.washCertificateStatus !== 'cancelled'" class="button is-small is-dark is-text" @click="showDownloadWashCertificate(object.id)" style="text-decoration-line: none;">
|
|
<span class="icon">
|
|
<i class="fas fa-file-download"></i>
|
|
</span>
|
|
<span>Download</span>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="buttons is-right">
|
|
<!-- If the status is pending, show the create wash certificate button -->
|
|
<button v-if="object.status === 'pending' && SessionUser.canAccessAdmin()" class="button is-small is-warning" @click="openUrlInNewTab('https://www.truckwash.dk/vaskecertifikat-administration/?wash_id=' + object.id + '&auth_key=' + meta.wash_certificate_token)">
|
|
<span class="icon">
|
|
<i class="fas fa-edit"></i>
|
|
</span>
|
|
</button>
|
|
<button v-if="(object.status === 'pending')" class="button is-small is-danger is-inverted" @click="showDeleteBooking(object)">
|
|
<span class="icon">
|
|
<i class="fas fa-trash"></i>
|
|
</span>
|
|
<span v-if="!SessionUser.canAccessAdmin()">Annuller</span>
|
|
</button>
|
|
<!-- If the status is completed, show a green checkmark -->
|
|
<button v-if="object.status === 'completed'" class="button is-small is-success is-inverted" >
|
|
<span class="icon">
|
|
<i class="fas fa-check"></i>
|
|
</span>
|
|
<span>Fuldført</span>
|
|
</button>
|
|
<!-- If the status is cancelled -->
|
|
<button v-else-if="object.status === 'cancelled'" class="button is-small is-danger is-inverted" >
|
|
<span>Annulleret</span>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</template>
|
|
</tr>
|
|
</tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<td colspan="13">Viser {{ objects.length }} bookinger {{ isSmall ? '' : 'i alt' }}</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
<!-- If the screen is mobile, show the mobile cards instead -->
|
|
<div class="columns is-multiline is-hidden-desktop mb-2" v-if="isSmall">
|
|
<div class="column is-one-third-tablet" v-for="object in objects" :key="object.id">
|
|
<div class="card">
|
|
<div class="card-content">
|
|
<div class="content">
|
|
<div class="is-pulled-right">
|
|
<button v-if="object.status === 'completed'" class="button is-small is-success is-inverted" >
|
|
<span class="icon">
|
|
<i class="fas fa-check"></i>
|
|
</span>
|
|
<span>Fuldført</span>
|
|
</button>
|
|
<button v-else-if="object.status === 'pending'" class="button is-small is-warning is-inverted" >
|
|
<span class="icon">
|
|
<i class="fas fa-clock"></i>
|
|
</span>
|
|
<span>Afventer</span>
|
|
</button>
|
|
<button v-if="(SessionUser.canAccessAdmin() && object.status === 'pending')" class="button is-small is-danger is-inverted" @click="showDeleteBooking(object)">
|
|
<span class="icon">
|
|
<i class="fas fa-trash"></i>
|
|
</span>
|
|
<span>Slet</span>
|
|
</button>
|
|
<button v-else-if="object.status === 'cancelled'" class="button is-small is-danger is-inverted" >
|
|
<span class="icon">
|
|
<i class="fas fa-ban"></i>
|
|
</span>
|
|
<span>Annulleret</span>
|
|
</button>
|
|
</div>
|
|
<p><strong>ID:</strong> {{ object.id }}</p>
|
|
<p><strong>Afdeling:</strong> {{ getDepartmentName(object.department) }}</p>
|
|
<p><strong>Dato:</strong> {{ object.date }}</p>
|
|
<p><strong>Reg. Trækker:</strong> {{ object.regNrTraekker }}</p>
|
|
<p><strong>Reg. Trailer:</strong> {{ object.regNrTrailer }}</p>
|
|
<p><strong>Reference:</strong> {{ object.reference_number }}</p>
|
|
<p><strong>Noter:</strong> {{ object.notes }}</p>
|
|
<p><strong>Skal hentes?</strong> {{ object.pickup_bool == 1 ? "Ja" : "Nej" }}</p>
|
|
<p v-if="object.status === 'completed'"><strong>Vaskecertifikat: </strong>
|
|
<button v-if="object.status === 'completed'" class="button is-small is-dark is-text" @click="showDownloadWashCertificate(object.id)" style="text-decoration-line: none;">
|
|
<span class="icon">
|
|
<i class="fas fa-file-download"></i>
|
|
</span>
|
|
<span>Download</span>
|
|
</button>
|
|
<button v-else-if="SessionUser.canAccessAdmin() && object.status !== 'cancelled'" class="button is-small is-warning" @click="openUrlInNewTab('https://www.truckwash.dk/vaskecertifikat-administration/?wash_id=' + object.id + '&auth_key=' + meta.wash_certificate_token)">
|
|
<span class="icon">
|
|
<i class="fas fa-edit"></i>
|
|
</span>
|
|
</button>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
|
|
</style> |