Add form handling, new booking flow, and demo branding check

Introduced form management with `Forms` object and new `FormDisplay` component. Updated booking flow with a new route and page for creating wash bookings. Added branding toggling logic for demo domain and enhanced user dashboard pagination with contextual buttons.
This commit is contained in:
Jepp9350
2025-03-12 14:37:25 +01:00
parent 7bdafee811
commit beb0919eae
11 changed files with 392 additions and 5 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

+270
View File
@@ -0,0 +1,270 @@
<script setup>
import { ref, defineProps, watch } from 'vue'
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {parseError, getError, clearErrors, hasError} from "@/components/request/HandleGlobalError.vue";
import ShowErrorField from "@/components/global/ShowErrorField.vue";
import Swal from "sweetalert2";
const props = defineProps({
form_identifier: {
type: String,
required: true
}
})
const isUserAdmin = ref(false);
const randomElementId = Math.random().toString(36).substring(7)
const validators = [
{
name: 'validateCustomerNumber',
description: 'the E-conomic customer number',
type: 'number',
required: true,
min: 1,
max: 999999999,
default: SessionUser.user.customer_number,
locked: true
},
{
name: 'validateEmail',
description: 'the email address',
type: 'email',
required: true,
},
{
name: 'validateString',
description: 'the string',
type: 'text',
required: true,
},
{
name: 'validateRegistrationNumber',
description: 'the registration number',
type: 'text',
required: true,
min: 3,
max: 12,
},
{
name: 'validateBoolean',
description: 'the boolean',
type: 'checkbox',
required: false,
},
{
name: 'validateDate',
description: 'the date',
type: 'date',
required: true,
},
{
name: 'validateArray',
description: 'the array',
type: 'array',
required: true,
},
{
name: 'validateObject',
description: 'the object',
type: 'object',
required: true,
},
{
name: 'validateInt',
description: 'the integer',
type: 'number',
required: true,
},
];
const isValidatorDefined = (validator) => {
return validators.find((v) => v.name === validator) !== undefined;
}
const getValidator = (validator) => {
return validators.find((v) => v.name === validator);
}
const fieldValues = ref({});
const meta = ref({
name: null,
description: null,
identifier: null,
});
const fields = ref([]);
const onSubmit = async () => {
console.log('submit');
// Submit the form
SessionUser.objects.forms.functions.submit(getForm()).then((response) => {
clearErrors(randomElementId);
console.log(response);
Swal.fire(
'Success',
'The form has been submitted',
'success'
);
}).catch((error) => {
console.error(error);
parseError(error, randomElementId);
});
}
const onReset = async () => {
console.log('reset');
// Reset the field values
fieldValues.value = {};
}
const onFieldChange = async (field, value) => {
console.log(field, value);
}
const getFieldName = (field) => {
try {
return SessionUser.objects.forms.meta.labels.fields.names[field.name].label;
}
catch (e) {
return field.name;
}
}
const toggleCheckbox = (field) => {
console.log(field);
// Set the checkbox value to false, if it is undefined
if (fieldValues.value[field.id] === undefined) {
fieldValues.value[field.id] = false;
}
fieldValues.value[field.id] = !fieldValues.value[field.id];
console.log(fieldValues.value[field.id]);
}
const isCheckboxChecked = (field) => {
// Check if the field is set in the fieldValues object
if (fieldValues.value[field.id] === undefined) {
fieldValues.value[field.id] = false;
}
return fieldValues.value[field.id] === true;
}
const isFieldLocked = (field) => {
const validator = getValidator(field.validation);
// Check if the locked property is set
if (validator.locked === undefined) {
console.log('locked is undefined', field);
return false;
}
let result = validator.locked;
console.log('locked', field, result, validator);
return result;
}
const getForm = () => {
// Add the missing fields to the fieldValues object
const defaultNullValues = [];
for (let i = 0; i < fields.value.length; i++) {
defaultNullValues[fields.value[i].id] = null;
}
return {
id: props.form_identifier,
...defaultNullValues,
...fieldValues.value
}
}
const setDefaultValues = () => {
console.log('setDefaultValues');
console.log(fields.value);
for (const field_index in fields.value) {
const key = field_index;
const field = fields.value[field_index];
const validator = getValidator(field.validation);
console.log(field, validator);
if (validator.default !== undefined) {
fieldValues.value[field.id] = validator.default;
}
}
}
// Get the form
SessionUser.objects.forms.get.single(props.form_identifier).then((response) => {
console.log(response);
meta.value = response.metadata;
fields.value = response.fields;
setDefaultValues();
}).catch((error) => {
console.error(error);
parseError(error, randomElementId);
});
watch(SessionUser.permissions, (permissions) => {
console.log('permissions changed', permissions);
console.log('Does the user have the admin permission?', permissions.includes('admin'));
isUserAdmin.value = permissions.includes('admin');
});
</script>
<template>
<form @submit.prevent="onSubmit" @reset.prevent="onReset">
<h1>{{ meta.name }}</h1>
<p>{{ meta.description }}</p>
<!-- Hidden fields -->
<input type="hidden" name="form_identifier" :value="props.form_identifier">
<!-- Fields -->
<div v-for="field in fields" :key="field.id" class="field">
<label class="label" :for="field.id">{{ getFieldName(field) }}</label>
<template v-if="isValidatorDefined(field.validation)">
<!-- Number -->
<input
class="input is-link"
v-if="getValidator(field.validation).type === 'number'"
:type="getValidator(field.validation).type"
:min="getValidator(field.validation).min ?? 0"
:max="getValidator(field.validation).max ?? ''"
:required="getValidator(field.validation).required"
:name="field.id" :id="field.id"
@change="onFieldChange(field, $event.target.value)"
v-model="fieldValues[field.id]"
v-bind:disabled="isFieldLocked(field)"
>
<!-- Text -->
<input class="input is-link" v-else-if="getValidator(field.validation).type === 'text'" :type="getValidator(field.validation).type" :required="getValidator(field.validation).required" :name="field.id" :id="field.id" @change="onFieldChange(field, $event.target.value)" v-model="fieldValues[field.id]" v-bind:disabled="isFieldLocked(field)">
<!-- Email -->
<input class="input is-link" v-else-if="getValidator(field.validation).type === 'email'" :type="getValidator(field.validation).type" :required="getValidator(field.validation).required" :name="field.id" :id="field.id" @change="onFieldChange(field, $event.target.value)" v-model="fieldValues[field.id]" :disabled="isFieldLocked(field)">
<!-- Checkbox -->
<div class="control" v-else-if="getValidator(field.validation).type === 'checkbox'" :type="getValidator(field.validation).type" :required="getValidator(field.validation).required" :name="field.id" :id="field.id" @change="onFieldChange(field, $event.target.checked)">
<input class="switch" :type="getValidator(field.validation).type" :required="getValidator(field.validation).required" :name="field.id" :id="field.id" @change="onFieldChange(field, $event.target.checked)" :checked="isCheckboxChecked(field)">
<label :for="field.id" @click="toggleCheckbox(field)" :disabled="isFieldLocked(field)"></label>
</div>
<!-- Date -->
<input class="input is-link" v-else-if="getValidator(field.validation).type === 'date'" :type="getValidator(field.validation).type" :required="getValidator(field.validation).required" :name="field.id" :id="field.id" @change="onFieldChange(field, $event.target.value)" v-model="fieldValues[field.id]" :disabled="isFieldLocked(field)">
</template>
<!-- If the validator is not defined -->
<template v-else>
<div class="message is-warning">
<div class="message-body">
<p>Validator is unknown for field `{{ field.name }}` with identifier `{{ field.id }}`</p>
</div>
</div>
</template>
</div>
<!-- Error handling -->
<show-error-field :error="randomElementId" v-if="hasError(randomElementId)" />
<!-- Submit and reset buttons -->
<div class="field">
<p class="control buttons">
<button class="button is-link" type="submit">{{ SessionUser.objects.global.language.submit }}</button>
<button class="button is-light" type="reset">{{ SessionUser.objects.global.language.reset }}</button>
</p>
</div>
</form>
</template>
<style scoped>
</style>
@@ -25,6 +25,8 @@ import PaginationDisplay from "@/components/displays/pagination/PaginationDispla
import BookingsTable from "@/components/displays/user/bookings/bookingsTable.vue";
import {departments} from "@/components/pagination/departmentTabs.vue";
import {ref, watch} from "vue";
import { Colors } from "@/ThemeConfig.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
const router = useRouter();
@@ -140,11 +142,21 @@ window.addEventListener('resize', () => {
<label class="label">{{ isSmall ? 'I dag' : 'Vis kun i dag' }}</label>
<div class="control">
<div class="field">
<input id="today" type="checkbox" class="switch is-rounded" @change="(event) => { setFilter('date', event.target.checked ? new Date().toISOString().split('T')[0] : '*') }" checked="checked" :class="{ 'is-success': showingToday }" />
<input id="today" type="checkbox" class="switch is-rounded" @change="(event) => { setFilter('date', event.target.checked ? new Date().toISOString().split('T')[0] : '*') }" checked="checked" :class="{ 'is-link': showingToday }" />
<label for="today"></label>
</div>
</div>
</div>
<!-- Create a new booking, if the route is /user -->
<div class="column is-narrow my-3" v-if="router.currentRoute.value.path.startsWith('/user')" :class="{ 'has-text-right': !isSmall }">
<button
class="button is-link"
@click="SessionUser.functions.redirectTo.user('/bookings/new')"
:style="{ 'color': Colors.buttons.textColor, 'background-color': Colors.buttons.backgroundColor }"
>
Ny booking
</button>
</div>
<!-- Only show the pending bookings (Switch, if the route is /admin) -->
<div class="column is-narrow my-3" v-if="router.currentRoute.value.path.startsWith('/admin')" :class="{ 'has-text-right': !isSmall }">
<label class="label">{{ isSmall ? 'Afventende' : 'Vis kun afventende' }}</label>
+5 -1
View File
@@ -156,6 +156,9 @@ if (props.menu_items.length > 0) {
menu_items.value = props.menu_items
}
// Check if the domain starts with jp-demo.truckwash.dk
const isDemo = window.location.hostname.startsWith('jp-demo.truckwash.dk');
</script>
<template>
@@ -165,7 +168,8 @@ if (props.menu_items.length > 0) {
>
<!-- Image -->
<div class="menu-image has-text-centered py-4 px-6">
<img src="@/assets/branding/truckwash-banner-white-compressed.png" alt="Truck Wash Logo" />
<img src="@/assets/branding/truckwash-banner-white-compressed.png" alt="Truck Wash Logo" v-if="!isDemo">
<img src="@/assets/branding/jp-lastvognsvask-white.png" alt="JP Lastvognsvask Logo" v-else>
</div>
<p class="menu-label"
:style="{ 'color': Colors.menus.parentTextColor, 'background-color': Colors.menus.parentBackgroundColor }"
@@ -19,6 +19,7 @@ import {CollectedOrderInvoices} from "@/components/session/token/SessionUser/Obj
import {DepartmentDailyReports} from "@/components/session/token/SessionUser/Objects/DepartmentDailyReports.vue";
import {Notifications} from "@/components/session/token/SessionUser/Objects/Notifications.vue";
import {Orders} from "@/components/session/token/SessionUser/Objects/Orders.vue";
import {Forms} from "@/components/session/token/SessionUser/Objects/Forms.vue";
import {ObjectsGlobal} from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
import {useRouter} from "vue-router";
import Swal from "sweetalert2";
@@ -184,6 +185,7 @@ export const SessionUser = {
department_daily_reports: DepartmentDailyReports,
notifications: Notifications,
orders: Orders,
forms: Forms,
global: ObjectsGlobal,
},
/** The user's token & authentication status */
@@ -0,0 +1,71 @@
<script>
import Swal from "sweetalert2";
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {ref} from "vue";
/**
* The Forms object
*/
export const Forms = {
meta: {
title: "Formularer",
icon: "fas fa-list",
description: "Oversigt over formularer",
endpoint: "/form",
labels: {
single: "formular",
multiple: "formularer",
fields: {
names: {
customer_number: {
label: "E-conomic kundenummer",
},
contact_email: {
label: "Kontakt email",
},
reference: {
label: "Reference",
},
registration_number_tractor: {
label: "Reg. nr. trækker",
},
registration_number_trailer: {
label: "Reg. nr. trailer",
},
wants_wash_certificate: {
label: "Ønsker vaskecertifikat ( Safety Seal )",
},
wants_wash_certificate_email: {
label: "Email til vaskecertifikat",
},
date: {
label: "Dato",
},
department_id: {
label: "Afdeling",
},
wants_pickup: {
label: "Ønsker afhentning",
},
notes: {
label: "Noter",
},
},
}
}
},
get: {
single: async (identifier) => {
return ObjectsGlobal.get.object(Forms.meta.endpoint, identifier);
}
},
functions: {
submit: async (form) => {
console.log(form);
return ObjectsGlobal.add.object(Forms.meta.endpoint, form);
},
},
};
</script>
@@ -28,6 +28,8 @@ export const ObjectsGlobal = {
nothing_to_do_all_set: "Sådan, der er ikke mere at gøre her!",
mark_as_completed: "Marker som fuldført",
delete: "Slet",
submit: "Indsend",
reset: "Nulstil",
},
parse: {
boolean: (value) => {
+3 -3
View File
@@ -2,9 +2,9 @@ import '@/themes/Dark.sass';
import '@popperjs/core';
//export const API_URL = 'https://truckwashdev.maintenancemode.cloud';
//export const API_URL = 'https://nnks.truckwash.dk';
export const IS_DEV = false;
//export const API_URL = 'https://api.truckwash.dk:4433';
export const API_URL = 'https://api.truckwash.dk';
export const IS_DEV = true;
export const API_URL = 'https://api.truckwash.dk:4433';
//export const API_URL = 'https://api.truckwash.dk';
import { createApp } from 'vue'
import App from './App.vue'
import store from './store/user.vue'
+8
View File
@@ -75,6 +75,7 @@ import DepartmentDailyReport
from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReport.vue";
import SuperUserDashboardProduct from "@/views/dashboards/superUserDashboard/products/SuperUserDashboardProduct.vue";
import UserOther from "@/views/dashboards/superUserDashboard/user/UserOther.vue";
import MyBookingsNew from "@/views/dashboards/userDashboard/bookings/MyBookingsNew.vue";
// Export the router as router
export const router = createRouter({
@@ -110,6 +111,13 @@ export const router = createRouter({
component: MyBookings,
meta: { middleware: authMiddleware }
},
{
name: 'mybookingsnew',
path: '/user/bookings/new',
component: MyBookingsNew,
meta: { middleware: authMiddleware }
},
{
name: 'myorders',
path: '/user/orders',
@@ -0,0 +1,18 @@
<script setup>
import BookingsPagination from "@/components/displays/pagination/models/UserDashboard/BookingsPagination.vue";
import UserDashboardPageWrapper from "@/views/dashboards/userDashboard/UserDashboardPageWrapper.vue";
import FormDisplay from "@/components/displays/FormDisplay.vue";
</script>
<template>
<div>
<UserDashboardPageWrapper title="Mine bookinger" subtitle="Book en ny vask">
<FormDisplay :form_identifier="'BOOK_WASH'" />
</UserDashboardPageWrapper>
</div>
</template>
<style scoped>
</style>