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:
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
@@ -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>
|
||||
|
||||
@@ -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
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user