**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:
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
+1
-2
@@ -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 på 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>
|
||||
Reference in New Issue
Block a user