**Add ICS handling, mobile menu toggle, and vehicle management**

Introduce ICS calendar support with parsing and fetching utilities. Improve mobile menu functionality with toggle expansion. Add vehicle management enhancements, including XLVask vehicle types and usage logs. Minor adjustments made to product and booking types, visual refinements, and new routes added.
This commit is contained in:
Jepp9350
2025-05-19 15:20:29 +02:00
parent 7751695a3c
commit b623332c31
23 changed files with 738 additions and 29 deletions
+1 -1
View File
@@ -163,7 +163,7 @@ const isAddonRestricted = (addon) => {
<div class="pt-2 mb-2 pr-6">
<div class="columns is-multiline pos-product-box" :class="{ 'is-selected': isSelected }">
<!-- Product image -->
<div class="column is-4-desktop">
<div class="column is-4-desktop is-hidden-touch">
<img :src="image" alt="Product image" class="image pos-product-image pl-5 mt-4" />
</div>
<!-- Product details -->
@@ -16,10 +16,15 @@ const props = defineProps({
type: Number,
required: true,
},
order_items: {
type: Array,
required: true,
},
});
import { Colors } from "@/ThemeConfig.vue";
import NextStep from "@/components/forms/department/pos/buttons/NextStep.vue";
import PrintInvoiceFromOrderItems from "@/components/forms/department/pos/buttons/PrintInvoiceFromOrderItems.vue";
// Readers variables
const readers = ref([]);
@@ -376,6 +381,17 @@ const showPaymentDetails = (paymentIntent) => {
</button>
</template>
</div>
<!-- Print invoice (if the payment intent has been captured) -->
<template v-if="StripeModule.paymentIntents.isPaymentIntentAmountReceived(StripeModule.paymentIntents.paymentIntent.value)">
<div class="column">
<PrintInvoiceFromOrderItems
v-bind:order_id="props.order_id"
v-bind:order-items="props.order_items"
v-bind:tax_percentage="getTaxRatePercentage(selectedTaxRate)"
v-bind:paid="true"
/>
</div>
</template>
{{ error }}
</div>
</template>
@@ -60,6 +60,7 @@ import PayWithStripeButton from "@/components/displays/department/pos/displays/P
label="Betal med Stripe"
:departmentId="department_id"
:order_id="order_id"
v-bind:order_items="order_items"
/>
</template>
</div>
@@ -33,6 +33,7 @@ const hasUpdatePermission = () => {
<thead>
<tr>
<th v-if="!isColumnHidden('id')">{{su_object.columns.id.label}}</th>
<th v-if="!isColumnHidden('product')">{{su_object.columns.product.label}}</th>
<th v-if="!isColumnHidden('name')">{{su_object.columns.name.label}}</th>
<th v-if="!isColumnHidden('description')">{{su_object.columns.description.label}}</th>
<th v-if="!isColumnHidden('duration')">{{su_object.columns.duration.label}}</th>
@@ -42,6 +43,18 @@ const hasUpdatePermission = () => {
<tbody>
<tr v-for="obj in objects" :key="obj.id">
<td v-if="!isColumnHidden('id')">{{obj.id}}</td>
<!-- Product -->
<EditableTableColumn
v-if="!isColumnHidden('product')"
:object="obj"
:loadList="loadList"
:editFunction="su_object.showEditObjectFieldForm"
column="product"
:permission-check-function="hasUpdatePermission"
:parse-function="(value) => {
return SessionUser.objects.products.functions.getProductName(value);
}"
/>
<!-- Label -->
<EditableTableColumn
v-if="!isColumnHidden('name')"
@@ -429,8 +429,8 @@ onMounted(() => {
<div>
<div class="columns is-multiline">
<div class="column is-12 is-flex is-justify-content-space-between">
<!-- Categories -->
<WhiteBox style="padding-bottom: 0; padding-top: 0.6rem;" class="px-0">
<!-- Categories - Desktop -->
<WhiteBox style="padding-bottom: 0; padding-top: 0.6rem;" class="px-0 is-hidden-touch">
<template #default>
<!-- Tabs -->
<div class="tabs" style="overflow-x: auto;">
@@ -461,6 +461,25 @@ onMounted(() => {
</div>
</template>
</WhiteBox>
<!-- Categories - Mobile -->
<WhiteBox class="is-hidden-desktop" style="padding-bottom: 0; padding-top: 0.6rem; width: 100%;">
<template #default>
<div class="select is-fullwidth mb-2">
<select
@change="hideRecommendedProducts();
setProductsCategory($event.target.value);
getProductCategory($event.target.value, department_id.valueOf())
.then((response) => {
products = response.data.data;
});"
>
<option v-for="category in categories" :key="category.identifier" :value="category.identifier ?? 'null'">
{{ category.name }}
</option>
</select>
</div>
</template>
</WhiteBox>
</div>
</div>
<div class="tw-scroll-minimal" style="overflow-y: auto; overflow-x: clip; max-height: 100vh;">
@@ -0,0 +1,65 @@
<script setup>
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { ref, defineProps } from 'vue';
const props = defineProps({
orderItems: {
type: Array,
required: true,
},
tax_percentage: {
type: Number,
required: true,
},
paid: {
type: Boolean,
default: false,
},
order_id: {
type: Number,
required: true,
},
});
const calculatePriceAfterTax = (price, tax_percentage) => {
// Calculate the price after tax
return price + (price * tax_percentage / 100);
};
const printInvoice = () => {
// Show a print dialog, with a table of the order items
// and the tax percentage
const printWindow = window.open('', '_blank');
printWindow.document.write('<html><head><title>Invoice #' + props.order_id + '</title></head>');
printWindow.document.write('<table border="1" style="width: 100%"><tr><th>Item</th><th>Amount</th><th>Price</th><th>Total</th></tr>');
props.orderItems.forEach(item => {
printWindow.document.write(`<tr><td>${item.product.name}</td><td>${item.quantity}</td><td>${item.price.toFixed(2)} DKK</td><td>${(item.price * item.quantity).toFixed(2)} DKK</td></tr>`);
});
// Add a row for the tax percentage
printWindow.document.write(`<tr><td colspan="3">Tax (${props.tax_percentage}%)</td><td>${props.orderItems.reduce((total, item) => total + (item.price * item.quantity), 0) * (props.tax_percentage / 100).toFixed(2)} DKK</td></tr>`);
// Add a row for the total
printWindow.document.write(`<tr><td colspan="3">Total</td><td>${props.orderItems.reduce((total, item) => total + (item.price * item.quantity), 0) * (1 + props.tax_percentage / 100).toFixed(2)} DKK</td></tr>`);
printWindow.document.write('</table>');
// Add a PAID / UNPAID label
if (props.paid) {
printWindow.document.write('<h2 style="color: green; text-align: center">PAID</h2>');
}
printWindow.document.write('</body></html>');
printWindow.document.close();
printWindow.focus();
printWindow.print();
printWindow.close();
};
</script>
<template>
<div class="buttons">
<button class="button is-primary" @click="printInvoice">
Print Invoice
</button>
</div>
</template>
<style scoped>
</style>
+1 -1
View File
@@ -177,7 +177,7 @@ const isDemo = false;
:style="{ 'background-color': Colors.menus.parentBackgroundColor, 'color': Colors.menus.parentTextColor }"
>
<!-- Image -->
<div class="menu-image has-text-centered py-4 px-6">
<div class="menu-image has-text-centered py-4 px-6 is-hidden-mobile">
<img src="@/assets/branding/truckwash-banner-white-compressed.png" alt="Truck Wash Logo" v-if="!isDemo">
<img src="@/assets/branding/truckcare-logo.png" alt="TruckCare logo" v-else>
</div>
+3 -1
View File
@@ -28,6 +28,7 @@ import {DepartmentTimeBookingsOpeningHours} from "@/components/session/token/Ses
import {DepartmentTimeBookingsTypes} from "@/components/session/token/SessionUser/Objects/DepartmentTimeBookingsTypes.vue";
import {DepartmentTimeBookingsEntries} from "@/components/session/token/SessionUser/Objects/DepartmentTimeBookingsEntries.vue";
import {DepartmentVariables} from "@/components/session/token/SessionUser/Objects/DepartmentVariables.vue";
import {XLVaskVehicleTypes} from "@/components/session/token/SessionUser/Objects/XLVaskVehicleTypes.vue";
import {ObjectsGlobal} from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
import {useRouter} from "vue-router";
import Swal from "sweetalert2";
@@ -210,9 +211,10 @@ export const SessionUser = {
department_time_bookings_types: DepartmentTimeBookingsTypes,
department_time_bookings_entries: DepartmentTimeBookingsEntries,
department_variables: DepartmentVariables,
xlvask_vehicle_types: XLVaskVehicleTypes,
global: ObjectsGlobal,
},
/** The user's token & authentication status */
/** The user's token and authentication status */
token: ref(null),
authenticated: ref(false),
/** The user's e-conomic data */
@@ -12,8 +12,8 @@ export const DepartmentTimeBookingsTypes = {
description: "Oversigt over afdelings tidsbookings typer.",
endpoint: "/department/timebookings/types",
labels: {
single: "booking type",
multiple: "booking typer",
single: "vasketype",
multiple: "vasketyper",
}
},
columns: {
@@ -40,8 +40,33 @@ export const DepartmentTimeBookingsTypes = {
label: "Navn",
type: "string",
sortable: true,
creation: {
required: false
}
},
product: {
label: "Produkt",
type: "select",
sortable: true,
creation: {
required: true
},
options: async () => {
// Get all the products, that has 'subscription_allowed' set to true
let products = await SessionUser.objects.products.get.all();
products = products.filter((product) => product.id <= 20);
// Add a default option (0, unknown)
products.unshift({
id: 0,
name: "Ukendt"
});
// Map the products to the options
return products.map((product) => {
return {
id: parseInt(product.id),
name: product.name
}
});
}
},
description: {
@@ -61,12 +86,14 @@ export const DepartmentTimeBookingsTypes = {
}
},
},
add: async (department, name, duration) => {
add: async (department, product, duration, name = null, description = null) => {
return ObjectsGlobal.add.object(
DepartmentTimeBookingsTypes.meta.endpoint,
{
department: parseInt(department),
name,
product: parseInt(product),
name: name,
description: description,
duration: parseInt(duration),
}
);
@@ -80,6 +107,14 @@ export const DepartmentTimeBookingsTypes = {
name
)
},
product: async (id, product) => {
return ObjectsGlobal.set.column(
DepartmentTimeBookingsTypes.meta.endpoint,
id,
"product",
parseInt(product)
)
},
description: async (id, description) => {
return ObjectsGlobal.set.column(
DepartmentTimeBookingsTypes.meta.endpoint,
@@ -13,10 +13,10 @@ export const ObjectsGlobal = {
},
language: {
title: (object) => {
return `Opret ${object.meta.labels.single}`;
return `Opret ${object.meta.labels.single.toLowerCase()}`;
},
field: (object, column) => {
return `Rediger ${object.columns[column].label}`;
return `Rediger ${object.columns[column].label.toLowerCase()}`;
},
split: "Opdel",
save: "Gem",
@@ -25,6 +25,7 @@ export const ObjectsGlobal = {
auto_start_on_lpr: "Auto-start ved LPR",
close: "Luk",
usage_log: "Brugslog",
import_all: "Importer alle",
note: "Note",
import: "Importer",
phone_country_code: "Landekode",
@@ -0,0 +1,165 @@
<script>
import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {ref} from "vue";
/**
* Local vehicle types cache
* @type {ref<null>}
*/
export const vehicle_types_cache = ref(null);
/**
* Get the vehicle type name
* @param id
* @param fallback
* @returns {string}
*/
export const getVehicleTypeName = (id, fallback = null) => {
if (vehicle_types_cache.value === null) {
vehicle_types_cache.value = [];
SessionUser.objects.xlvask_vehicle_types.get.all().then((vehicle_types) => {
vehicle_types_cache.value = vehicle_types;
});
}
const vehicle_type = vehicle_types_cache.value.find((vehicle_type) => vehicle_type.vehicleTypeId === id);
return vehicle_type ? vehicle_type.name : fallback === null ? SessionUser.objects.global.language.no_data : fallback;
}
/**
* The XLVaskVehicleTypes object
*/
export const XLVaskVehicleTypes = {
meta: {
title: "Køretøjstyper",
icon: "fas fa-list",
description: "Oversigt over køretøjstyper",
endpoint: "/modules/xlvask/internal/vehicle-types",
labels: {
single: "køretøjstype",
multiple: "køretøjstyper"
}
},
columns: {
id: {
label: "ID",
type: "number",
sortable: true,
creation: {
required: false
}
},
vehicleTypeId: {
label: "Vehicle Type ID",
type: "string",
sortable: true,
creation: {
required: true
}
},
product: {
label: "Produkt",
type: "select",
sortable: true,
creation: {
required: true
},
options: async () => {
return SessionUser.objects.vehicles.columns.type.options();
},
},
name: {
label: "Navn",
type: "string",
sortable: true,
creation: {
required: false
}
},
},
add: async (vehicleTypeId, product, name = null) => {
return ObjectsGlobal.add.object(
XLVaskVehicleTypes.meta.endpoint,
{
vehicleTypeId: vehicleTypeId,
product: parseInt(product),
name: name,
}
);
},
set: {
vehicleTypeId: async (id, vehicleTypeId) => {
return ObjectsGlobal.set.column(
XLVaskVehicleTypes.meta.endpoint,
parseInt(id),
"vehicleTypeId",
vehicleTypeId
)
},
product: async (id, product) => {
return ObjectsGlobal.set.column(
XLVaskVehicleTypes.meta.endpoint,
parseInt(id),
"product",
parseInt(product)
)
},
name: async (id, name) => {
return ObjectsGlobal.set.column(
XLVaskVehicleTypes.meta.endpoint,
parseInt(id),
"name",
name
)
},
},
get: {
all: async () => {
return ObjectsGlobal.get.objects(XLVaskVehicleTypes.meta.endpoint);
},
single: async (id) => {
return ObjectsGlobal.get.object(XLVaskVehicleTypes.meta.endpoint, id);
}
},
delete: async (id) => {
return ObjectsGlobal.delete.object(XLVaskVehicleTypes.meta.endpoint, id);
},
functions: {
getVehicleTypeName: getVehicleTypeName,
/**
* Show the delete object form
* @param id
* @param onAfterSubmit
* @returns {Promise<SweetAlertResult<Awaited<any>>>}
*/
showDeleteObjectForm: (id, onAfterSubmit = null) => {
return ObjectsGlobal.showDeleteObjectForm(XLVaskVehicleTypes, id, onAfterSubmit);
},
},
/**
* Show the create object form
* @param onAfterSubmit
* @param lockedValues
* @returns {Promise<SweetAlertResult<Awaited<any>>>}
*/
showCreateObjectForm: (onAfterSubmit = null, lockedValues = {}) => {
return ObjectsGlobal.showCreateObjectForm(XLVaskVehicleTypes, onAfterSubmit, lockedValues);
},
/**
* Show the edit object field form
* @param id
* @param column
* @param value
* @param onAfterSubmit
* @returns {Promise<SweetAlertResult<Awaited<any>>>}
*/
showEditObjectFieldForm: (id, column, value, onAfterSubmit = null) => {
return ObjectsGlobal.showEditObjectFieldForm(
XLVaskVehicleTypes,
id,
column,
value,
onAfterSubmit
);
}
};
</script>
+1 -1
View File
@@ -21,7 +21,7 @@ export const getProductCategory = (category, departmentId = null) => {
return null;
}
// If the category is null, return all products
if (!category) {
if (!category || category === 'null') {
return axios.get(API_URL + '/products' + (departmentId ? '?department_id=' + departmentId : ''), {
headers: {
Authorization: `Bearer ${token}`
@@ -25,15 +25,12 @@ const showCreateBooking = () => {
<template>
<div class="timebookings-calendar">
<h1>Time Bookings Calendar</h1>
<p>This is the Time Bookings Calendar component.</p>
<p>Department ID: {{ departmentId }}</p>
<button @click="showCreateBooking">Create Booking</button>
<Calendar events="[]"
calendarId="timebookings-calendar"
@eventClick="(event) => {
console.log('Event clicked:', event);
}"/>
<button class="button" @click="showCreateBooking">Create Booking</button>
</div>
</template>
@@ -38,10 +38,15 @@ console.log('Created event:', calendar.functions.getEvents());
// Set the click event
calendar.eventClick.setEventClick(handleEventClick);
const icsUrl = 'https://cloud.truckwash.dk/remote.php/dav/public-calendars/r3DPYmCLfRp7NLgT?export';
// Set the ICS URL, fetch and parse the data.
calendar.functions.setIcsUrl(icsUrl);
calendar.functions.fetchAndParseIcsData();
</script>
<template>
<iframe width="100%" height="600" src="https://cloud.truckwash.dk/apps/calendar/embed/r3DPYmCLfRp7NLgT/timeGridWeek/now"></iframe>
<!--
<FullCalendar
:options="{
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin, listPlugin],
@@ -52,11 +57,11 @@ calendar.eventClick.setEventClick(handleEventClick);
},
initialView: 'dayGridMonth',
events: [
...calendar.functions.getEvents()
...calendar.functions.getEvents(),
],
eventClick: calendar.eventClick.executeEventClick,
}"
/>
/> -->
</template>
<style scoped>
@@ -1,9 +1,9 @@
<script>
import { ref, computed } from 'vue';
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {ref} from 'vue';
// Define reactive properties
const id = ref(null);
const icsUrl = ref(null);
const events = ref([]);
const customFunctions = ref({});
const eventClick = ref(null);
@@ -96,6 +96,65 @@ const functions_event = {
},
};
/** Functions -> ICS management */
const functions_ics = {
setIcsUrl: (url) => {
// Set the ICS URL
icsUrl.value = url;
},
getIcsUrl: () => {
// Get the ICS URL
return icsUrl.value;
},
clearIcsUrl: () => {
// Clear the ICS URL
icsUrl.value = null;
},
fetchIcsData: async () => {
// Fetch the ICS data from the URL
if (icsUrl.value) {
try {
const response = await fetch(icsUrl.value);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.text();
} catch (error) {
console.error('Error fetching ICS data:', error);
}
} else {
console.warn('No ICS URL set');
}
},
parseIcsData: (data) => {
// Parse the ICS data and return events
const events = [];
const lines = data.split('\n');
let event = {};
lines.forEach(line => {
if (line.startsWith('BEGIN:VEVENT')) {
event = {};
} else if (line.startsWith('END:VEVENT')) {
events.push(event);
} else {
const [key, value] = line.split(':');
if (key && value) {
event[key.trim()] = value.trim();
}
}
});
return events;
},
fetchAndParseIcsData: async () => {
// Fetch and parse the ICS data
const data = await functions_ics.fetchIcsData();
if (data) {
return functions_ics.parseIcsData(data);
}
return [];
},
};
/** Functions -> Custom functions */
const functions_custom = {
addCustomFunction: (name, func) => {
@@ -126,6 +185,7 @@ const functions_custom = {
const functions_default = {
...functions_id,
...functions_event,
...functions_ics,
...functions_custom,
};
+7
View File
@@ -98,6 +98,7 @@ import DepartmentTimeBookingsTypes
from "@/views/dashboards/departmentDashboard/modules/time-bookings/DepartmentTimeBookingsTypes.vue";
import DepartmentModulesSetup
from "@/views/dashboards/superUserDashboard/department/modules/DepartmentModulesSetup.vue";
import Vehicle from "@/views/dashboards/superUserDashboard/vehicle/Vehicle.vue";
// Export the router as router
export const router = createRouter({
@@ -482,6 +483,12 @@ export const router = createRouter({
component: Vehicles,
meta: { middleware: superUserMiddleware }
},
{
name: 'vehiclesvehicle',
path: '/superuser/vehicles/:reg',
component: Vehicle,
meta: { middleware: superUserMiddleware }
},
{
name: 'rolesPermissions',
path: '/superuser/roles/:roleId',
+37 -6
View File
@@ -1,7 +1,7 @@
<script setup>
import { Colors } from "@/ThemeConfig.vue";
import { defineProps } from "vue";
import {defineProps, ref} from "vue";
import { defineSlots } from "vue";
import UserNavigationTop from "@/components/displays/user/UserNavigationTop.vue";
import Footer from "@/components/global/Footer.vue";
@@ -13,6 +13,11 @@ defineProps({
})
const slots = defineSlots()
const IS_EXPANDED = ref(false)
const TOGGLE_EXPANDED = () => {
IS_EXPANDED.value = !IS_EXPANDED.value
}
</script>
<template>
@@ -56,22 +61,48 @@ const slots = defineSlots()
</div>
</div>
</div>
<!-- Left menu -->
<!-- Left menu / Mobile menu-->
<div class="column is-2 is-continuous-to-bottom"
:style="{ 'background-color': Colors.menus.parentBackgroundColor, 'color': Colors.menus.textColor }"
>
<slot name="left-menu"></slot>
<!-- Development version display -->
<div class="is-fixed-bottom" v-if="IS_DEV">
<div class="has-text-centered">
<div class="is-hidden-mobile">
<slot name="left-menu"></slot>
<!-- Development version display -->
<div class="is-fixed-bottom" v-if="IS_DEV">
<div class="has-text-centered">
<span class="is-size-6 has-text-warning">
IS_DEV ON
</span>
</div>
</div>
</div>
<!-- Mobile menu -->
<div class="is-fixed-bottom is-hidden-desktop is-hidden-tablet is-clickable" @click="TOGGLE_EXPANDED">
<div class="has-text-centered">
<span class="is-size-6 has-text-warning">
<!-- Expanded toggle button -->
<button
class="button is-small is-warning"
:style="{ 'background-color': Colors.menus.parentBackgroundColor, 'color': Colors.menus.textColor }"
>
<span class="icon">
<i :class="IS_EXPANDED ? 'fas fa-angle-up' : 'fas fa-angle-down'"></i>
</span>
<span>
{{ IS_EXPANDED? 'Collapse':'Expand' }}
</span>
</button>
</span>
<!-- Expanded menu -->
<div v-if="IS_EXPANDED" class="is-hidden-desktop">
<slot name="left-menu"></slot>
</div>
</div>
</div>
</div>
<!-- Page content -->
<div
v-if="!IS_EXPANDED"
class="column is-10"
:style="{ 'background-color': Colors.global.backgroundColor, 'color': Colors.global.textColor }"
style="min-height: 100vh;"
@@ -29,6 +29,7 @@ import WhiteBox from "@/components/displays/boxes/WhiteBox.vue";
import ButtonsBox from "@/components/displays/boxes/ButtonsBox.vue";
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
import ShowErrorField from "@/components/global/ShowErrorField.vue";
import PrintInvoiceFromOrderItems from "@/components/forms/department/pos/buttons/PrintInvoiceFromOrderItems.vue";
// Get the id from the route
const orderId = ref(router.currentRoute.value.params.orderId);
@@ -457,6 +458,7 @@ const isCompleted = () => {
<span class="icon"><i class="fas fa-file-invoice"></i></span>
<span>Hent faktura</span>
</button> -->
<!--<PrintInvoiceFromOrderItems v-bind:orderItems="order_items" tax_percentage="25" v-bind:paid="isStripeInvoicePaid()" v-bind:order_id="orderId" /> -->
</div>
</div>
</div>
@@ -16,12 +16,11 @@ import {IS_DEV} from "@/main.js";
<NotFoundFallBackPageWrapper :exists="SessionUser.functions.getDepartmentIdFromUrl() && SessionUser.canAccessDepartment(SessionUser.functions.getDepartmentIdFromUrl())" error="Please select a department, that you have access to. If you believe this is an error, please contact support.">
<div class="message is-warning">
<div class="message-body">
Denne side er under udvikling, vi venter i øjeblikket tilbagemelding fra grafisk designere.
<strong>Udvikles</strong> Dette modul er under udvikling og vil snart være tilgængeligt.
</div>
</div>
<!-- Calendar -->
<Calendar
v-if="IS_DEV"
:departmentId="SessionUser.functions.getDepartmentIdFromUrl()"
/>
</NotFoundFallBackPageWrapper>
@@ -14,6 +14,7 @@ import ConfigurationSelect from "@/components/displays/superuser/configuration/C
import SuperUserDashboardDepartmentModulesNavigation
from "@/views/dashboards/superUserDashboard/department/modules/SuperUserDashboardDepartmentModulesNavigation.vue";
import ConfigurationSwitch from "@/components/displays/superuser/configuration/ConfigurationSwitch.vue";
import ConfigurationInput from "@/components/displays/superuser/configuration/ConfigurationInput.vue";
// Get the department from the route
const router = useRouter()
@@ -153,6 +154,21 @@ const setDepartmentStripeLocation = async (locationId) => {
});
}"
></ConfigurationSwitch>
<!-- Calendar password -->
<ConfigurationInput
title="Booking system (tidsbaseret) kodeord"
description="Kodeord til booking system (tidsbaseret)"
v-bind:value="getVariableValue('bookingsystem_time_based_password')"
:on-save="(value) => {
SessionUser.objects.department_variables.add(
departmentId,
'bookingsystem_time_based_password',
value
).then(() => {
getDepartmentVariables();
});
}"
></ConfigurationInput>
</template>
</ConfigurationCategory>
</div>
@@ -70,6 +70,29 @@ const onClickImportVehicle = (vehicle) => {
customer_number,
)
}
const onClickImportAllVehicles = () => {
console.log('Clicked on import all vehicles');
// Get the vehicles from XLVask
SessionUser.superUser.modules.xlvask.functions.getVehicles(
props.xlVaskCustomerObject.customerId,
).then((response) => {
let matchingVehicles = response.data.data;
if (matchingVehicles.length !== 0) {
matchingVehicles.forEach((vehicle) => {
// Add the vehicle to the customer
console.log('Adding vehicle to customer', vehicle, props.xlVaskCustomerObject.externId);
SessionUser.objects.vehicles.add(
0,
vehicle.registrationNumber,
false, // Since XLVask API haven't yet implemented the wash types, we don't know which one to use
props.xlVaskCustomerObject.externId,
)
});
}
}
).catch((error => {}));
}
</script>
<template>
@@ -118,6 +141,9 @@ const onClickImportVehicle = (vehicle) => {
:loadList="loadList"
column="vehicleTypeId"
:permission-check-function="hasUpdatePermission"
:parse-function="(value) => {
return SessionUser.objects.xlvask_vehicle_types.functions.getVehicleTypeName(value, value);
}"
/>
<!-- Active -->
<EditableTableColumn
@@ -160,6 +186,15 @@ const onClickImportVehicle = (vehicle) => {
:icon="SessionUser.objects.global.icons.import"
/>
</RequiresPermission>
<RequiresPermission
permission="add_vehicle_other"
>
<ActionSettingsWheelItem
:label="SessionUser.objects.global.language.import_all + ' ' + SessionUser.objects.vehicles.meta.labels.multiple.toLowerCase()"
:click-action="onClickImportAllVehicles"
:icon="SessionUser.objects.global.icons.import"
/>
</RequiresPermission>
</template>
</ActionSettingsWheelButton>
</td>
@@ -0,0 +1,19 @@
<script setup>
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { ref } from 'vue';
import { useRouter } from "vue-router";
import XLVaskUsageLog from "@/views/dashboards/superUserDashboard/vehicle/displays/XLVaskUsageLog.vue";
const router = useRouter();
// Get the registration from the route
const reg = ref(router.currentRoute.value.params.reg);
</script>
<template>
<XLVaskUsageLog v-bind:reg="reg"/>
</template>
<style scoped>
</style>
@@ -0,0 +1,221 @@
<script setup>
import { ref, defineProps, defineEmits, onMounted } from 'vue';
import { SessionUser } from "@/components/session/token/SessionUser.vue";
const props = defineProps({
reg: {
type: String,
required: true,
},
});
/** Dynamic variables */
const usageLog = ref(null);
const getUsageLog = async () => {
SessionUser.superUser.modules.xlvask.functions.getUsageLog(
null,
props.reg,
).then(
(response) => {
if (response.status === 200) {
console.log("Usage log response:", response);
usageLog.value = response.data.data;
} else {
console.error("Error fetching usage log:", response);
}
}
).catch(
(error) => {
console.error("Error fetching usage log:", error);
}
);
};
const example = {
"WashId": "892ae789-aeea-4bda-9374-cf931290aefd",
"CustomerId": "59440200",
"Customer": "DITOBUS EXCURSIONS A/S",
"VatNumber": "31171520",
"Location": "Hvidovre",
"Hall": "Hvidovre_1",
"HallId": "845d29a1-a7d2-4e3b-bbc3-2b13242d744a",
"StartTime": "2024-01-26T14:46:53.067",
"FinishTime": "2024-01-26T14:54:08.653",
"RegistrationNumber": "BJ22227",
"VehicleType": "Bus/autocamper, M",
"IdentificationType": "LPR",
"IdentificationId": "BJ22227",
"Info": "BJ22227",
"Updated": "",
"Prepaid": false,
"FinishStatus": 1,
"CustomerGuid": "21ba156a-b2d2-44be-8398-4b67d66003d6",
"VehicleId": "0584ef66-3deb-491e-9b82-0a28cfc20e9e",
"WashItems": [
{
"WashItemId": "399b25c1-5c03-4f25-b5e9-00731e9b94c4",
"ExternalProductId": null,
"ExternalProductName": null,
"OriginalProductName": "Ikke HT dysebom bag",
"Unit": "stk",
"UnitPrice": 0,
"Count": 1,
"Discount": 65,
"PriceExVat": 0,
"Vat": 0,
"PriceIncVat": 0
},
{
"WashItemId": "ef3ca812-bbe5-4cf3-916f-06fd17c04225",
"ExternalProductId": null,
"ExternalProductName": "Spot Free",
"OriginalProductName": "Skylning med RO",
"Unit": "stk",
"UnitPrice": 35,
"Count": 1,
"Discount": 65,
"PriceExVat": 35,
"Vat": 3.06,
"PriceIncVat": 15.31
},
{
"WashItemId": "5c4ec0d9-cffc-45eb-9213-406dbcb8c975",
"ExternalProductId": null,
"ExternalProductName": null,
"OriginalProductName": "2-børstevask",
"Unit": "stk",
"UnitPrice": 0,
"Count": 1,
"Discount": 65,
"PriceExVat": 0,
"Vat": 0,
"PriceIncVat": 0
},
{
"WashItemId": "f7bd7404-f5a3-4a59-8e85-53b284df3260",
"ExternalProductId": null,
"ExternalProductName": null,
"OriginalProductName": "Halleje",
"Unit": "min",
"UnitPrice": 0,
"Count": 1,
"Discount": 65,
"PriceExVat": 0,
"Vat": 0,
"PriceIncVat": 0
},
{
"WashItemId": "6dba6c24-8191-4c2b-913c-7366518fb41d",
"ExternalProductId": null,
"ExternalProductName": null,
"OriginalProductName": "Stor bil",
"Unit": "stk",
"UnitPrice": 559,
"Count": 1,
"Discount": 65,
"PriceExVat": 559,
"Vat": 48.91,
"PriceIncVat": 244.56
},
{
"WashItemId": "61d11d87-2ee4-4a4d-890b-a0870ae1a924",
"ExternalProductId": null,
"ExternalProductName": null,
"OriginalProductName": "HT sider",
"Unit": "stk",
"UnitPrice": 0,
"Count": 1,
"Discount": 65,
"PriceExVat": 0,
"Vat": 0,
"PriceIncVat": 0
},
{
"WashItemId": "ec6eb1c4-b58c-457f-8224-b674cf41dc29",
"ExternalProductId": null,
"ExternalProductName": null,
"OriginalProductName": "EU spejl",
"Unit": "stk",
"UnitPrice": 0,
"Count": 1,
"Discount": 65,
"PriceExVat": 0,
"Vat": 0,
"PriceIncVat": 0
},
{
"WashItemId": "54b4deec-be3e-4cb2-b68d-b9af9cff6fcf",
"ExternalProductId": null,
"ExternalProductName": null,
"OriginalProductName": "HT chassis",
"Unit": "stk",
"UnitPrice": 0,
"Count": 1,
"Discount": 65,
"PriceExVat": 0,
"Vat": 0,
"PriceIncVat": 0
}
]
}
onMounted(() => {
getUsageLog();
});
</script>
<template>
<h1>XLVask Usage Log</h1>
<table class="table">
<!-- Table header -->
<thead>
<tr>
<th>Registration Number</th>
<th>Start Time</th>
<th>End Time</th>
<th>Location</th>
</tr>
</thead>
<!-- Table body -->
<tbody>
<template v-for="(usage, index) in usageLog" :key="index">
<tr>
<td>{{ usage.RegistrationNumber }}</td>
<td>{{ usage.StartTime }}</td>
<td>{{ usage.FinishTime }}</td>
<td>{{ usage.Location }}</td>
</tr>
<template v-if="usage.WashItems">
<tr>
<td colspan="4">
<table class="table is-fullwidth is-striped is-hoverable is-bordered">
<thead>
<tr>
<th>Wash Item</th>
<th>Count</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, itemIndex) in usage.WashItems" :key="itemIndex">
<td>{{ item.OriginalProductName }}</td>
<td>{{ item.Count }}</td>
<td>{{ item.PriceIncVat }}</td>
</tr>
</tbody>
</table>
</td>
</tr>
</template>
</template>
</tbody>
</table>
{{ props.reg }}
{{ usageLog }}
</template>
<style scoped>
</style>