Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d435c6d60a | ||
|
|
278298ecd2 | ||
|
|
dec90bbbbf | ||
|
|
a4b9d2f04d |
@@ -5,6 +5,7 @@ import { useWindowSize } from "@vueuse/core";
|
|||||||
import { BMessage } from "buefy";
|
import { BMessage } from "buefy";
|
||||||
|
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
type DateRange = {
|
type DateRange = {
|
||||||
startDate: Date,
|
startDate: Date,
|
||||||
@@ -115,23 +116,11 @@ const availableYears = computed(() => (
|
|||||||
));
|
));
|
||||||
|
|
||||||
const formatDateInputValue = (date: Date) => {
|
const formatDateInputValue = (date: Date) => {
|
||||||
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
return formatLocalDateOnly(date);
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const year = date.getFullYear();
|
|
||||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
||||||
const day = String(date.getDate()).padStart(2, "0");
|
|
||||||
return `${year}-${month}-${day}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseDateInputValue = (value: string) => {
|
const parseDateInputValue = (value: string) => {
|
||||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
return parseLocalDateOnly(value);
|
||||||
if (!match) {
|
|
||||||
return new Date(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSameDateInputValue = (left: Date, right: Date) => (
|
const isSameDateInputValue = (left: Date, right: Date) => (
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import PosLastScannedLicensePlatesV2 from "@/components/displays/department/pos/
|
|||||||
import PosDesktopOrderBookingSelectorModal from "@/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue";
|
import PosDesktopOrderBookingSelectorModal from "@/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue";
|
||||||
import PosDesktopCustomerConflictModal from "@/components/displays/department/pos/steps/elements/PosDesktopCustomerConflictModal.vue";
|
import PosDesktopCustomerConflictModal from "@/components/displays/department/pos/steps/elements/PosDesktopCustomerConflictModal.vue";
|
||||||
import PosDesktopDuplicateWarning from "@/components/displays/department/pos/steps/elements/PosDesktopDuplicateWarning.vue";
|
import PosDesktopDuplicateWarning from "@/components/displays/department/pos/steps/elements/PosDesktopDuplicateWarning.vue";
|
||||||
|
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import { parsePosRouteSearch } from "@/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js";
|
import { parsePosRouteSearch } from "@/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js";
|
||||||
import {
|
import {
|
||||||
@@ -377,8 +378,8 @@ const fetchDuplicateOrdersForContext = async (context) => {
|
|||||||
try {
|
try {
|
||||||
const response = await SessionUser.request(SessionUser.objects.orders.meta.endpoint, "GET", {
|
const response = await SessionUser.request(SessionUser.objects.orders.meta.endpoint, "GET", {
|
||||||
filters: `reg_1:${normalizedContext.reg1},department_id:${department_id.value},created_at-date_from:${
|
filters: `reg_1:${normalizedContext.reg1},department_id:${department_id.value},created_at-date_from:${
|
||||||
new Date().toISOString().split("T")[0]
|
todayLocalDateOnly()
|
||||||
},created_at-date_to:${new Date().toISOString().split("T")[0]}`,
|
},created_at-date_to:${todayLocalDateOnly()}`,
|
||||||
limit: 5,
|
limit: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+44
-7
@@ -22,7 +22,7 @@ import PosDepartmentStep2MobileVehicleSelection
|
|||||||
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
|
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
|
||||||
import PosDepartmentStepMobile2FloatingCart
|
import PosDepartmentStepMobile2FloatingCart
|
||||||
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2FloatingCart.vue";
|
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2FloatingCart.vue";
|
||||||
import { isAddonRestricted, canBuyAdditionalServices } from "@/components/shop/POSDepartmentProcess.vue";
|
import { isAddonRestricted, canBuyAdditionalServices, isProductRestricted } from "@/components/shop/POSDepartmentProcess.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
@@ -46,6 +46,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const canSelectAdditionalItems = computed(() => canBuyAdditionalServices());
|
||||||
const checked = ref(props.defaultChecked);
|
const checked = ref(props.defaultChecked);
|
||||||
// Function to generate a summary from the last order
|
// Function to generate a summary from the last order
|
||||||
function generateSummary(order: PosOrder): string {
|
function generateSummary(order: PosOrder): string {
|
||||||
@@ -72,6 +73,12 @@ const displaySubtitle = computed(() => {
|
|||||||
});
|
});
|
||||||
// Emit event on toggle
|
// Emit event on toggle
|
||||||
function onToggle(isOpen: boolean) {
|
function onToggle(isOpen: boolean) {
|
||||||
|
if (isOpen && !canSelectAdditionalItems.value) {
|
||||||
|
checked.value = false;
|
||||||
|
pos.views.additionalItemSelection.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
checked.value = isOpen;
|
checked.value = isOpen;
|
||||||
// Open the additional item selection view if toggled open
|
// Open the additional item selection view if toggled open
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
@@ -118,17 +125,45 @@ const getAvailableAdditionalItems = () => {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
const availableAdditionalItems = ref<Addon[]>(getAvailableAdditionalItems());
|
const availableAdditionalItems = ref<Addon[]>(getAvailableAdditionalItems());
|
||||||
|
const isAdditionalItemRestricted = (product: PosProduct) => {
|
||||||
|
if (!canSelectAdditionalItems.value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isProductRestricted(product)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return isAddonRestricted(convertProductToAddon(product));
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(canSelectAdditionalItems, (canSelect) => {
|
||||||
|
if (!canSelect) {
|
||||||
|
checked.value = false;
|
||||||
|
pos.views.additionalItemSelection.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Computed property to filter out restricted additional items based on customer attributes
|
// Computed property to filter out restricted additional items based on customer attributes
|
||||||
const filteredAdditionalItems = computed(() => {
|
const filteredAdditionalItems = computed(() => {
|
||||||
// If additional services are restricted, return empty array
|
// If additional services are restricted, return empty array
|
||||||
if (!canBuyAdditionalServices()) {
|
if (!canSelectAdditionalItems.value) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
// Filter out individually restricted addons
|
// Filter out individually restricted addons
|
||||||
return availableAdditionalItems.value.filter((addon: Addon) => !isAddonRestricted(addon));
|
return availableAdditionalItems.value.filter((addon: Addon) => {
|
||||||
|
if (isAddonRestricted(addon)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (addon.product && isProductRestricted(addon.product)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
const onClickAddOtherProduct = () => {
|
const onClickAddOtherProduct = () => {
|
||||||
|
if (!canSelectAdditionalItems.value) {
|
||||||
|
pos.views.additionalItemSelection.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
pos.views.additionalItemSelection.value = !pos.views.additionalItemSelection.value;
|
pos.views.additionalItemSelection.value = !pos.views.additionalItemSelection.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +194,9 @@ watch(() => pos.transactionItems.additionalItems.value, (newVal) => {
|
|||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
const onClickAddProduct = async (product: PosProduct) => {
|
const onClickAddProduct = async (product: PosProduct) => {
|
||||||
// If the product requires note, open note input.
|
if (isAdditionalItemRestricted(product)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
pos.transactionItems.addAdditionalItem(product);
|
pos.transactionItems.addAdditionalItem(product);
|
||||||
// If the view is fullscreen, close it after adding
|
// If the view is fullscreen, close it after adding
|
||||||
//if (pos.views.additionalItemSelection.value) {
|
//if (pos.views.additionalItemSelection.value) {
|
||||||
@@ -171,7 +208,7 @@ const onClickAddProduct = async (product: PosProduct) => {
|
|||||||
<template>
|
<template>
|
||||||
<div data-testid="pos-mobile-additional-items">
|
<div data-testid="pos-mobile-additional-items">
|
||||||
<!-- Minimal view, when not set as fullscreen view -->
|
<!-- Minimal view, when not set as fullscreen view -->
|
||||||
<WhiteBoxCard :toggleable="pos.transactionItems.additionalItems.value && pos.transactionItems.additionalItems.value.length == 0"
|
<WhiteBoxCard :toggleable="canSelectAdditionalItems && pos.transactionItems.additionalItems.value && pos.transactionItems.additionalItems.value.length == 0"
|
||||||
:defaultOpen="pos.transactionItems.additionalItems.value && pos.transactionItems.additionalItems.value.length > 0"
|
:defaultOpen="pos.transactionItems.additionalItems.value && pos.transactionItems.additionalItems.value.length > 0"
|
||||||
@toggle="onToggle"
|
@toggle="onToggle"
|
||||||
:forceState="(pos.transactionItems.additionalItems.value && pos.transactionItems.additionalItems.value.length > 0) ? true : (pos.views.additionalItemSelection.value)"
|
:forceState="(pos.transactionItems.additionalItems.value && pos.transactionItems.additionalItems.value.length > 0) ? true : (pos.views.additionalItemSelection.value)"
|
||||||
@@ -201,7 +238,7 @@ const onClickAddProduct = async (product: PosProduct) => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<template #footer>
|
<template #footer v-if="canSelectAdditionalItems">
|
||||||
<!-- Select other product button -->
|
<!-- Select other product button -->
|
||||||
<a class="card-footer-item" data-testid="pos-mobile-additional-items-open" @click="onClickAddOtherProduct">
|
<a class="card-footer-item" data-testid="pos-mobile-additional-items-open" @click="onClickAddOtherProduct">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
@@ -212,7 +249,7 @@ const onClickAddProduct = async (product: PosProduct) => {
|
|||||||
</template>
|
</template>
|
||||||
</WhiteBoxCard>
|
</WhiteBoxCard>
|
||||||
<!-- Fullscreen view, when selecting other products -->
|
<!-- Fullscreen view, when selecting other products -->
|
||||||
<template v-else>
|
<template v-else-if="canSelectAdditionalItems">
|
||||||
<div data-testid="pos-mobile-additional-items-selection">
|
<div data-testid="pos-mobile-additional-items-selection">
|
||||||
<!-- Categories of products -->
|
<!-- Categories of products -->
|
||||||
<PosDepartmentStep2MobileVehicleSelection :onAddProduct="onClickAddProduct" :onSearchClick="() => console.warn('AdditionalItem Search Clicked')"/><!-- :asAddons="true" :addons="availableAdditionalItems" @update:addons="availableAdditionalItems = $event"/>-->
|
<PosDepartmentStep2MobileVehicleSelection :onAddProduct="onClickAddProduct" :onSearchClick="() => console.warn('AdditionalItem Search Clicked')"/><!-- :asAddons="true" :addons="availableAdditionalItems" @update:addons="availableAdditionalItems = $event"/>-->
|
||||||
|
|||||||
+2
-1
@@ -12,6 +12,7 @@ import PosDepartmentStepMobileButtonNextStep from "@/components/displays/departm
|
|||||||
import SessionUser from "@/components/session/token/SessionUser.vue";
|
import SessionUser from "@/components/session/token/SessionUser.vue";
|
||||||
import { PosOrder } from "@/components/displays/department/pos/steps/mobile/objects/PosOrder.vue";
|
import { PosOrder } from "@/components/displays/department/pos/steps/mobile/objects/PosOrder.vue";
|
||||||
import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue";
|
import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue";
|
||||||
|
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
// Define the close event to emit when the component is closed
|
// Define the close event to emit when the component is closed
|
||||||
const emit = defineEmits(["close"]);
|
const emit = defineEmits(["close"]);
|
||||||
/** Display variables */
|
/** Display variables */
|
||||||
@@ -126,7 +127,7 @@ const liveTransactions = ref(null);
|
|||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
|
|
||||||
const syncListTransactionHistory = async () => {
|
const syncListTransactionHistory = async () => {
|
||||||
let dateToday = new Date().toISOString().split("T")[0]; // Get today's date in YYYY-MM-DD format
|
let dateToday = todayLocalDateOnly(); // Get today's date in YYYY-MM-DD format
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
// Fetch the list of orders created today for the current department
|
// Fetch the list of orders created today for the current department
|
||||||
SessionUser.objects.orders.get
|
SessionUser.objects.orders.get
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { formatLocalDateOnly } from '@/services/dateOnly.js';
|
||||||
type startEndDate = {
|
type startEndDate = {
|
||||||
start: Date; // The start date of the range
|
start: Date; // The start date of the range
|
||||||
end: Date; // The end date of the range
|
end: Date; // The end date of the range
|
||||||
@@ -27,7 +28,7 @@ const datePresetFunctions = {
|
|||||||
month: {
|
month: {
|
||||||
// Get the first day of the month based on the provided date first day at 00:00:01
|
// Get the first day of the month based on the provided date first day at 00:00:01
|
||||||
firstDayOfMonth: (date: Date): Date => {
|
firstDayOfMonth: (date: Date): Date => {
|
||||||
return new Date(date.getFullYear(), date.getMonth(), 2, 0, 0, 1, 0); // Set to the first day of the month at 00:00:01
|
return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0); // Set to the first day of the month at 00:00:00
|
||||||
},
|
},
|
||||||
// Get the last day of the month based on the provided date last day at 23:59:59
|
// Get the last day of the month based on the provided date last day at 23:59:59
|
||||||
lastDayOfMonth: (date: Date): Date => {
|
lastDayOfMonth: (date: Date): Date => {
|
||||||
@@ -116,7 +117,7 @@ export const datePresets = <datePreset[]>[
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const convertToISO = (date: Date): string => {
|
const convertToISO = (date: Date): string => {
|
||||||
return date.toISOString().split('T')[0]; // Convert to YYYY-MM-DD format
|
return formatLocalDateOnly(date); // Convert to YYYY-MM-DD format
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dateFunctions = {
|
export const dateFunctions = {
|
||||||
@@ -150,4 +151,4 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template></template>
|
<template></template>
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import ShowErrorField from "@/components/global/ShowErrorField.vue";
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { BSwitch } from "buefy";
|
import { BSwitch } from "buefy";
|
||||||
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
|
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
|
||||||
|
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -63,7 +64,7 @@ const parseInitialDate = (value) => {
|
|||||||
return new Date();
|
return new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = new Date(`${value}T00:00:00`);
|
const parsed = parseLocalDateOnly(value);
|
||||||
return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
|
return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -129,7 +130,7 @@ const dateFrom = ref(parseInitialDate(props.initialDateFrom));
|
|||||||
const dateTo = ref(parseInitialDate(props.initialDateTo));
|
const dateTo = ref(parseInitialDate(props.initialDateTo));
|
||||||
|
|
||||||
const parsedDate = (date) => {
|
const parsedDate = (date) => {
|
||||||
return new Date(date);
|
return parseLocalDateOnly(date);
|
||||||
};
|
};
|
||||||
|
|
||||||
const reloadScheduled = ref(false);
|
const reloadScheduled = ref(false);
|
||||||
@@ -148,14 +149,14 @@ const actions = {
|
|||||||
from: {
|
from: {
|
||||||
select: (date) => {
|
select: (date) => {
|
||||||
dateFrom.value = parsedDate(date);
|
dateFrom.value = parsedDate(date);
|
||||||
setFilter("StartTime-date_from", date.toISOString().split("T")[0], false);
|
setFilter("StartTime-date_from", formatLocalDateOnly(date), false);
|
||||||
scheduleReload();
|
scheduleReload();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
to: {
|
to: {
|
||||||
select: (date) => {
|
select: (date) => {
|
||||||
dateTo.value = parsedDate(date);
|
dateTo.value = parsedDate(date);
|
||||||
setFilter("StartTime-date_to", date.toISOString().split("T")[0], false);
|
setFilter("StartTime-date_to", formatLocalDateOnly(date), false);
|
||||||
scheduleReload();
|
scheduleReload();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+4
-4
@@ -109,8 +109,8 @@ import OrdersTable from "@/components/displays/department/pos/orders/ordersTable
|
|||||||
import {SessionUser} from "@/components/session/token/SessionUser.vue";
|
import {SessionUser} from "@/components/session/token/SessionUser.vue";
|
||||||
import PaginationDisplayFilters from "@/components/displays/pagination/PaginationDisplayFilters.vue";
|
import PaginationDisplayFilters from "@/components/displays/pagination/PaginationDisplayFilters.vue";
|
||||||
import DatePeriodSelector from "@/components/displays/buttons/DatePeriodSelector.vue";
|
import DatePeriodSelector from "@/components/displays/buttons/DatePeriodSelector.vue";
|
||||||
import {now} from "@vueuse/core";
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { formatLocalDateOnly, parseLocalDateOnly, todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -176,8 +176,8 @@ if (props.autoLoad) {
|
|||||||
const date_from = ref(null);
|
const date_from = ref(null);
|
||||||
const date_to = ref(null);
|
const date_to = ref(null);
|
||||||
const onDateRangeSelected = (newSelectionStartDate, newSelectionToDate) => {
|
const onDateRangeSelected = (newSelectionStartDate, newSelectionToDate) => {
|
||||||
const formattedStartDate = newSelectionStartDate.toISOString().split("T")[0];
|
const formattedStartDate = formatLocalDateOnly(newSelectionStartDate);
|
||||||
const formattedEndDate = newSelectionToDate.toISOString().split("T")[0];
|
const formattedEndDate = formatLocalDateOnly(newSelectionToDate);
|
||||||
date_from.value = formattedStartDate;
|
date_from.value = formattedStartDate;
|
||||||
date_to.value = formattedEndDate;
|
date_to.value = formattedEndDate;
|
||||||
setFilter("created_at-date_from", formattedStartDate, true);
|
setFilter("created_at-date_from", formattedStartDate, true);
|
||||||
@@ -361,7 +361,7 @@ const doesEndpointMatch = (matcher) => {
|
|||||||
<!-- Shortcuts for date filters -->
|
<!-- Shortcuts for date filters -->
|
||||||
<DatePeriodSelector :on-selection-change="onDateRangeSelected"
|
<DatePeriodSelector :on-selection-change="onDateRangeSelected"
|
||||||
:visibility="{ showDailySelector: false, showWeeklySelector: false, showMultipleMonthWarning: false, showUpdateButton: false, showMonthSelector: false, showStartDate: false, showEndDate: false, showSelectionValidity: false, showYearSelector: false, showToLabel: false }"
|
:visibility="{ showDailySelector: false, showWeeklySelector: false, showMultipleMonthWarning: false, showUpdateButton: false, showMonthSelector: false, showStartDate: false, showEndDate: false, showSelectionValidity: false, showYearSelector: false, showToLabel: false }"
|
||||||
v-bind:selection="{ startDate: date_from ? new Date(date_from) : new Date(now()), endDate: date_to ? new Date(date_to) : new Date(now()) }"/>
|
v-bind:selection="{ startDate: parseLocalDateOnly(date_from || todayLocalDateOnly()), endDate: parseLocalDateOnly(date_to || todayLocalDateOnly()) }"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {ref, watch} from "vue";
|
|||||||
import { Colors } from "@/ThemeConfig.vue";
|
import { Colors } from "@/ThemeConfig.vue";
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -52,7 +53,7 @@ watch(() => getFilter("status"), (value) => {
|
|||||||
// If the route starts with /user, set the endpoint to /user/bookings
|
// If the route starts with /user, set the endpoint to /user/bookings
|
||||||
if (router.currentRoute.value.path.startsWith("/user")) {
|
if (router.currentRoute.value.path.startsWith("/user")) {
|
||||||
setEndpoint("/user/bookings", false);
|
setEndpoint("/user/bookings", false);
|
||||||
setFilter("date", new Date().toISOString().split("T")[0], false);
|
setFilter("date", todayLocalDateOnly(), false);
|
||||||
setOrder("created_at", "desc");
|
setOrder("created_at", "desc");
|
||||||
}
|
}
|
||||||
// If the route starts with /superuser, set the endpoint to /bookings
|
// If the route starts with /superuser, set the endpoint to /bookings
|
||||||
@@ -81,7 +82,7 @@ loadList();
|
|||||||
const showingToday = ref(true);
|
const showingToday = ref(true);
|
||||||
|
|
||||||
watch(() => getFilter("date"), (value) => {
|
watch(() => getFilter("date"), (value) => {
|
||||||
showingToday.value = value === new Date().toISOString().split("T")[0];
|
showingToday.value = value === todayLocalDateOnly();
|
||||||
});
|
});
|
||||||
|
|
||||||
// If the screen is mobile, set the is small variable to true
|
// If the screen is mobile, set the is small variable to true
|
||||||
@@ -151,7 +152,7 @@ const showNewOrderBookingsPortal = () => {
|
|||||||
<label class="label">{{ isSmall ? t('common.today') : t('pagination.show_only_today') }}</label>
|
<label class="label">{{ isSmall ? t('common.today') : t('pagination.show_only_today') }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<div class="field">
|
<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-link': showingToday }" />
|
<input id="today" type="checkbox" class="switch is-rounded" @change="(event) => { setFilter('date', event.target.checked ? todayLocalDateOnly() : '*') }" checked="checked" :class="{ 'is-link': showingToday }" />
|
||||||
<label for="today"></label>
|
<label for="today"></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,4 +189,4 @@ const showNewOrderBookingsPortal = () => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import OrderBookingsTable from "@/views/dashboards/userDashboard/bookings/displa
|
|||||||
import { departments, getDepartments } from "@/components/pagination/departmentTabs.vue";
|
import { departments, getDepartments } from "@/components/pagination/departmentTabs.vue";
|
||||||
import { Colors } from "@/ThemeConfig.vue";
|
import { Colors } from "@/ThemeConfig.vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { endOfLocalDate, startOfLocalDate, todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
/**
|
/**
|
||||||
@@ -52,11 +53,9 @@ const onOnlyTodayFilterChange = (event, autoLoadList = true) => {
|
|||||||
setFilter("datetime-date_from", null, false);
|
setFilter("datetime-date_from", null, false);
|
||||||
setFilter("datetime-date_to", null, false);
|
setFilter("datetime-date_to", null, false);
|
||||||
} else {
|
} else {
|
||||||
const startOfDay = new Date().setHours(0, 0, 0, 0);
|
|
||||||
const endOfDay = new Date().setHours(23, 59, 59, 999);
|
|
||||||
//setFilter('datetime', null, false);
|
//setFilter('datetime', null, false);
|
||||||
setFilter("datetime-date_from", new Date(startOfDay).toISOString(), false);
|
setFilter("datetime-date_from", startOfLocalDate(val).toISOString(), false);
|
||||||
setFilter("datetime-date_to", new Date(endOfDay).toISOString(), false);
|
setFilter("datetime-date_to", endOfLocalDate(val).toISOString(), false);
|
||||||
}
|
}
|
||||||
if (autoLoadList) {
|
if (autoLoadList) {
|
||||||
loadList();
|
loadList();
|
||||||
@@ -95,7 +94,7 @@ onMounted(() => {
|
|||||||
departmentFilter.value = value;
|
departmentFilter.value = value;
|
||||||
} else if (key === "only_today" && value === true) {
|
} else if (key === "only_today" && value === true) {
|
||||||
setFilterKey = false; // Since the only_today filter is handled separately
|
setFilterKey = false; // Since the only_today filter is handled separately
|
||||||
onOnlyTodayFilterChange({ target: { value: new Date().toISOString().split("T")[0] } }, false);
|
onOnlyTodayFilterChange({ target: { value: todayLocalDateOnly() } }, false);
|
||||||
}
|
}
|
||||||
if (setFilterKey) {
|
if (setFilterKey) {
|
||||||
setFilter(key, value, false);
|
setFilter(key, value, false);
|
||||||
@@ -170,7 +169,7 @@ onMounted(() => {
|
|||||||
<div class="select">
|
<div class="select">
|
||||||
<select :value="onlyTodayFilter" @change="onOnlyTodayFilterChange">
|
<select :value="onlyTodayFilter" @change="onOnlyTodayFilterChange">
|
||||||
<option value="*">{{ t("common.all") }}</option>
|
<option value="*">{{ t("common.all") }}</option>
|
||||||
<option :value="new Date().toISOString().split('T')[0]">{{ t("common.yes") }}</option>
|
<option :value="todayLocalDateOnly()">{{ t("common.yes") }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,7 +192,7 @@ onMounted(() => {
|
|||||||
@change="
|
@change="
|
||||||
(event) => {
|
(event) => {
|
||||||
onOnlyTodayFilterChange({
|
onOnlyTodayFilterChange({
|
||||||
target: { value: event.target.checked ? new Date().toISOString().split('T')[0] : '*' },
|
target: { value: event.target.checked ? todayLocalDateOnly() : '*' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -226,7 +225,7 @@ onMounted(() => {
|
|||||||
@change="
|
@change="
|
||||||
(event) => {
|
(event) => {
|
||||||
onOnlyTodayFilterChange(
|
onOnlyTodayFilterChange(
|
||||||
{ target: { value: event.target.checked ? new Date().toISOString().split('T')[0] : '*' } },
|
{ target: { value: event.target.checked ? todayLocalDateOnly() : '*' } },
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
onOrderIdFilterChange({ target: { value: event.target.checked ? 'is null' : '*' } });
|
onOrderIdFilterChange({ target: { value: event.target.checked ? 'is null' : '*' } });
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ loadList();
|
|||||||
:columnLabels="{ type: SessionUser.objects.vehicles.columns.type.label }"
|
:columnLabels="{ type: SessionUser.objects.vehicles.columns.type.label }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="$slots.actions" class="vehicles-pagination__add-action">
|
||||||
|
<label class="label is-small"> </label>
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
<!-- Create a new vehicle, if the route is /user -->
|
<!-- Create a new vehicle, if the route is /user -->
|
||||||
<div class="vehicles-pagination__add-action"
|
<div class="vehicles-pagination__add-action"
|
||||||
v-if="router.currentRoute.value.path.startsWith('/user')">
|
v-if="router.currentRoute.value.path.startsWith('/user')">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import PaginationDisplayItemColumn from "@/components/displays/pagination/PaginationDisplayItemColumn.vue";
|
import PaginationDisplayItemColumn from "@/components/displays/pagination/PaginationDisplayItemColumn.vue";
|
||||||
import { ref, computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -19,7 +20,7 @@ defineExpose({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const formattedDate = computed(() => {
|
const formattedDate = computed(() => {
|
||||||
return date.value.toISOString().split('T')[0];
|
return formatLocalDateOnly(date.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// This component is used to display a date input in a pagination display item column.
|
// This component is used to display a date input in a pagination display item column.
|
||||||
@@ -36,8 +37,7 @@ const formattedDate = computed(() => {
|
|||||||
class="input"
|
class="input"
|
||||||
:value="formattedDate"
|
:value="formattedDate"
|
||||||
@input="(e) => {
|
@input="(e) => {
|
||||||
const newDate = new Date(e.target.value);
|
const newDate = parseLocalDateOnly(e.target.value);
|
||||||
console.log('Selected date:', newDate);
|
|
||||||
emit('update:date', newDate);
|
emit('update:date', newDate);
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
@@ -47,4 +47,4 @@ const formattedDate = computed(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { SessionUser } from '@/components/session/token/SessionUser.vue';
|
import { SessionUser } from '@/components/session/token/SessionUser.vue';
|
||||||
|
|
||||||
@@ -10,7 +9,7 @@ import PaginationDisplayTemplateButton
|
|||||||
from "@/components/displays/pagination/templates/PaginationDisplayTemplateButton.vue";
|
from "@/components/displays/pagination/templates/PaginationDisplayTemplateButton.vue";
|
||||||
import { datePresets , dateFunctions} from '@/components/displays/pagination/PaginationDisplayDates.vue';
|
import { datePresets , dateFunctions} from '@/components/displays/pagination/PaginationDisplayDates.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
defineProps({
|
||||||
startDate: {
|
startDate: {
|
||||||
type: Date,
|
type: Date,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -46,6 +45,7 @@ function updateStartDate(date: Date) {
|
|||||||
// Update the end date model when the date is changed
|
// Update the end date model when the date is changed
|
||||||
function updateEndDate(date: Date) {
|
function updateEndDate(date: Date) {
|
||||||
endDateModel.value = date;
|
endDateModel.value = date;
|
||||||
|
emits('update:endDate', date);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -79,4 +79,4 @@ function updateEndDate(date: Date) {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { showDownloadWashCertificate } from "@/components/shop/DownloadWashCerti
|
|||||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||||
import Swal from 'sweetalert2';
|
import Swal from 'sweetalert2';
|
||||||
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
|
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
|
||||||
|
import { todayLocalDateOnly, yesterdayLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
const redirectBookingObjectPage = (objectId) => {
|
const redirectBookingObjectPage = (objectId) => {
|
||||||
// Send the user to the object page
|
// Send the user to the object page
|
||||||
window.location.href = `/user/bookings/${objectId}`;
|
window.location.href = `/user/bookings/${objectId}`;
|
||||||
@@ -237,7 +238,8 @@ window.addEventListener('resize', () => {
|
|||||||
* To organize the bookings, we want to show the bookings that are created today first, then yesterday, then all other days
|
* To organize the bookings, we want to show the bookings that are created today first, then yesterday, then all other days
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
const currentDate = new Date().toISOString().split("T")[0];
|
const currentDate = todayLocalDateOnly();
|
||||||
|
const previousDate = yesterdayLocalDateOnly();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort the bookings by date
|
* Sort the bookings by date
|
||||||
@@ -362,7 +364,7 @@ const canUserEditObject = (object) => {
|
|||||||
</span>
|
</span>
|
||||||
<span class="has-text-grey">
|
<span class="has-text-grey">
|
||||||
<!-- Human readable date (Today, Yesterday, etc.) -->
|
<!-- Human readable date (Today, Yesterday, etc.) -->
|
||||||
{{ object.date === currentDate ? $t('tables.bookings.today') : object.date === new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().split("T")[0] ? $t('tables.bookings.yesterday') : object.date }}
|
{{ object.date === currentDate ? $t('tables.bookings.today') : object.date === previousDate ? $t('tables.bookings.yesterday') : object.date }}
|
||||||
<!-- Number of bookings on the date -->
|
<!-- Number of bookings on the date -->
|
||||||
({{ $t('tables.bookings.bookings_count', { count: objects.filter((booking) => booking.date === object.date).length }) }})
|
({{ $t('tables.bookings.bookings_count', { count: objects.filter((booking) => booking.date === object.date).length }) }})
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, nextTick, onMounted, ref } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import CustomerSearchSelect from "@/components/search/economic/CustomerSearchSelect.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(["close", "created"]);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const selectedCustomer = ref(null);
|
||||||
|
const registrationNumber = ref("");
|
||||||
|
const vehicleType = ref("");
|
||||||
|
const washSubscription = ref(false);
|
||||||
|
const reference = ref("");
|
||||||
|
const vehicleTypeOptions = ref([]);
|
||||||
|
const isLoadingVehicleTypes = ref(false);
|
||||||
|
const isSubmitting = ref(false);
|
||||||
|
const errorMessage = ref("");
|
||||||
|
const vehicleTypeError = ref("");
|
||||||
|
|
||||||
|
const getCustomerNumber = (customer) => {
|
||||||
|
const parsedValue = Number.parseInt(
|
||||||
|
String(customer?.customerNumber ?? customer?.customer_number ?? customer?.customer_id ?? customer?.id ?? ""),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedCustomerNumber = computed(() => getCustomerNumber(selectedCustomer.value));
|
||||||
|
|
||||||
|
const normalizedVehicleType = computed(() => {
|
||||||
|
if (vehicleType.value === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedValue = Number.parseInt(String(vehicleType.value), 10);
|
||||||
|
return Number.isInteger(parsedValue) && parsedValue >= 0 ? parsedValue : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizedRegistrationNumber = computed(() => {
|
||||||
|
return registrationNumber.value.trim().toUpperCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
const canSubmit = computed(() => {
|
||||||
|
return (
|
||||||
|
!isSubmitting.value &&
|
||||||
|
!isLoadingVehicleTypes.value &&
|
||||||
|
selectedCustomerNumber.value !== null &&
|
||||||
|
normalizedRegistrationNumber.value.length > 0 &&
|
||||||
|
normalizedVehicleType.value !== null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseErrorMessage = (error) => {
|
||||||
|
return SessionUser.functions.parseErrorMessage(error) || t("vehicles.add_modal.error");
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadVehicleTypes = async () => {
|
||||||
|
isLoadingVehicleTypes.value = true;
|
||||||
|
vehicleTypeError.value = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
vehicleTypeOptions.value = await SessionUser.objects.vehicles.columns.type.options();
|
||||||
|
} catch (error) {
|
||||||
|
vehicleTypeError.value = parseErrorMessage(error) || t("vehicles.add_modal.type_load_error");
|
||||||
|
} finally {
|
||||||
|
isLoadingVehicleTypes.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
if (!isSubmitting.value) {
|
||||||
|
emit("close");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitVehicle = async () => {
|
||||||
|
if (!canSubmit.value) {
|
||||||
|
errorMessage.value = t("vehicles.add_modal.validation_error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmitting.value = true;
|
||||||
|
errorMessage.value = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await SessionUser.objects.vehicles.add(
|
||||||
|
normalizedVehicleType.value,
|
||||||
|
normalizedRegistrationNumber.value,
|
||||||
|
washSubscription.value,
|
||||||
|
selectedCustomerNumber.value,
|
||||||
|
reference.value.trim() || null
|
||||||
|
);
|
||||||
|
|
||||||
|
emit("created", response);
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.value = parseErrorMessage(error);
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadVehicleTypes();
|
||||||
|
await nextTick();
|
||||||
|
document.getElementById("superuser-add-vehicle-customer-search")?.focus();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="modal is-active" data-testid="superuser-add-vehicle-modal">
|
||||||
|
<div class="modal-background" @click="closeModal"></div>
|
||||||
|
<div class="modal-card superuser-add-vehicle-modal">
|
||||||
|
<header class="modal-card-head">
|
||||||
|
<p class="modal-card-title">{{ t("vehicles.add_modal.title") }}</p>
|
||||||
|
<button
|
||||||
|
class="delete"
|
||||||
|
type="button"
|
||||||
|
:aria-label="t('common.close')"
|
||||||
|
data-testid="superuser-add-vehicle-close"
|
||||||
|
@click="closeModal"
|
||||||
|
></button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="modal-card-body">
|
||||||
|
<div v-if="errorMessage" class="notification is-danger is-light" data-testid="superuser-add-vehicle-error">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CustomerSearchSelect
|
||||||
|
v-model="selectedCustomer"
|
||||||
|
input-id="superuser-add-vehicle-customer-search"
|
||||||
|
test-id-prefix="superuser-add-vehicle-customer"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
:placeholder="t('vehicles.add_modal.customer_placeholder')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="superuser-add-vehicle-registration">{{ t("vehicles.form.license_plate") }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
id="superuser-add-vehicle-registration"
|
||||||
|
v-model="registrationNumber"
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
:placeholder="t('vehicles.add_modal.registration_placeholder')"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
data-testid="superuser-add-vehicle-registration"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="superuser-add-vehicle-type">{{ t("vehicles.form.type") }}</label>
|
||||||
|
<div class="control" :class="{ 'is-loading': isLoadingVehicleTypes }">
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select
|
||||||
|
id="superuser-add-vehicle-type"
|
||||||
|
v-model="vehicleType"
|
||||||
|
:disabled="isSubmitting || isLoadingVehicleTypes || vehicleTypeOptions.length === 0"
|
||||||
|
data-testid="superuser-add-vehicle-type"
|
||||||
|
>
|
||||||
|
<option disabled value="">{{ t("vehicles.add_modal.type_placeholder") }}</option>
|
||||||
|
<option v-for="option in vehicleTypeOptions" :key="option.id" :value="option.id">
|
||||||
|
{{ option.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-if="vehicleTypeError" class="help is-danger" data-testid="superuser-add-vehicle-type-error">
|
||||||
|
{{ vehicleTypeError }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input
|
||||||
|
v-model="washSubscription"
|
||||||
|
type="checkbox"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
data-testid="superuser-add-vehicle-wash-subscription"
|
||||||
|
/>
|
||||||
|
{{ t("objects.vehicles.columns.wash_subscription") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="superuser-add-vehicle-reference">{{ t("common.reference") }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
id="superuser-add-vehicle-reference"
|
||||||
|
v-model="reference"
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
:placeholder="t('vehicles.add_modal.reference_placeholder')"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
data-testid="superuser-add-vehicle-reference"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="modal-card-foot is-justify-content-flex-end">
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
type="button"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
data-testid="superuser-add-vehicle-cancel"
|
||||||
|
@click="closeModal"
|
||||||
|
>
|
||||||
|
{{ t("common.cancel") }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button is-link"
|
||||||
|
type="button"
|
||||||
|
:class="{ 'is-loading': isSubmitting }"
|
||||||
|
:disabled="!canSubmit"
|
||||||
|
data-testid="superuser-add-vehicle-submit"
|
||||||
|
@click="submitVehicle"
|
||||||
|
>
|
||||||
|
{{ t("vehicles.add_modal.submit") }}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.superuser-add-vehicle-modal {
|
||||||
|
max-width: min(44rem, calc(100vw - 2rem));
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card-body {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -19,17 +19,17 @@ const SENSITIVE_QUERY_KEYS = /authorization|password|passwd|secret|token|api[_-]
|
|||||||
const FALLBACK_LABELS = {
|
const FALLBACK_LABELS = {
|
||||||
"error_report.button": "Report error",
|
"error_report.button": "Report error",
|
||||||
"error_report.title": "Report error",
|
"error_report.title": "Report error",
|
||||||
"error_report.subtitle": "Send the current screen and recent error details to support.",
|
"error_report.subtitle": "Send recent error details to support. A screenshot is attached when available.",
|
||||||
"error_report.before_error": "What were you doing before the error occurred?",
|
"error_report.before_error": "What were you doing before the error occurred?",
|
||||||
"error_report.expected": "What did you expect would happen?",
|
"error_report.expected": "What did you expect would happen?",
|
||||||
"error_report.actual": "What actually happened?",
|
"error_report.actual": "What actually happened?",
|
||||||
"error_report.before_error_placeholder": "Describe the action you were taking, for example opening orders or selecting a customer.",
|
"error_report.before_error_placeholder": "Describe the action you were taking, for example opening orders or selecting a customer.",
|
||||||
"error_report.expected_placeholder": "Describe the result you expected to see.",
|
"error_report.expected_placeholder": "Describe the result you expected to see.",
|
||||||
"error_report.actual_placeholder": "Describe what you saw instead, including any error text.",
|
"error_report.actual_placeholder": "Describe what you saw instead, including any error text.",
|
||||||
"error_report.consent": "I accept that the current app screen, recent request errors, Vue errors, browser details, and my answers are collected for troubleshooting.",
|
"error_report.consent": "I accept that recent request errors, Vue errors, browser details, my answers, and an app screenshot when available are collected for troubleshooting.",
|
||||||
"error_report.submit": "Submit report",
|
"error_report.submit": "Submit report",
|
||||||
"error_report.submitted": "Error report submitted.",
|
"error_report.submitted": "Error report submitted.",
|
||||||
"error_report.capture_failed": "The screen capture failed. Please try again.",
|
"error_report.capture_failed": "The screen capture failed. The report will be sent without a screenshot.",
|
||||||
"error_report.submit_failed": "The error report could not be submitted.",
|
"error_report.submit_failed": "The error report could not be submitted.",
|
||||||
"error_report.required": "All fields and data collection acceptance are required.",
|
"error_report.required": "All fields and data collection acceptance are required.",
|
||||||
};
|
};
|
||||||
@@ -277,15 +277,18 @@ const submit = async () => {
|
|||||||
|
|
||||||
isSubmitting.value = true;
|
isSubmitting.value = true;
|
||||||
try {
|
try {
|
||||||
let screenshot = "";
|
let screenshot = null;
|
||||||
try {
|
try {
|
||||||
screenshot = await captureScreenshot();
|
screenshot = await captureScreenshot();
|
||||||
} catch {
|
} catch {
|
||||||
captureError.value = tr("error_report.capture_failed");
|
captureError.value = tr("error_report.capture_failed");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = buildContext();
|
const context = buildContext();
|
||||||
|
context.screenshot_attachment = {
|
||||||
|
status: screenshot ? "stored" : "capture_failed",
|
||||||
|
attached: Boolean(screenshot),
|
||||||
|
};
|
||||||
await submitErrorReport({
|
await submitErrorReport({
|
||||||
before_error: form.before_error.trim(),
|
before_error: form.before_error.trim(),
|
||||||
expected: form.expected.trim(),
|
expected: form.expected.trim(),
|
||||||
@@ -340,7 +343,7 @@ const submit = async () => {
|
|||||||
<div v-if="submitted" class="notification is-success is-light" data-testid="error-report-submitted">
|
<div v-if="submitted" class="notification is-success is-light" data-testid="error-report-submitted">
|
||||||
{{ tr("error_report.submitted") }}
|
{{ tr("error_report.submitted") }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="captureError" class="notification is-danger is-light">
|
<div v-if="captureError" class="notification is-warning is-light" data-testid="error-report-capture-warning">
|
||||||
{{ captureError }}
|
{{ captureError }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="submitError" class="notification is-danger is-light">
|
<div v-if="submitError" class="notification is-danger is-light">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ref, watch, onMounted } from "vue";
|
|||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { isAccessibleVisibleNamedDepartment, sortByDepartmentPriorityOrder } from "@/services/departmentVisibility.js";
|
import { isAccessibleVisibleNamedDepartment, sortByDepartmentPriorityOrder } from "@/services/departmentVisibility.js";
|
||||||
|
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
// Get the department ID from the URL
|
// Get the department ID from the URL
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const department_id = route.params.departmentId || SessionUser.functions.getDepartmentIdFromUrl() || null;
|
const department_id = route.params.departmentId || SessionUser.functions.getDepartmentIdFromUrl() || null;
|
||||||
@@ -73,9 +74,9 @@ const emphasis = ref({
|
|||||||
"department:" +
|
"department:" +
|
||||||
department_id +
|
department_id +
|
||||||
",status:pending,date-date_from:" +
|
",status:pending,date-date_from:" +
|
||||||
new Date().toISOString().split("T")[0] +
|
todayLocalDateOnly() +
|
||||||
",date-date_to:" +
|
",date-date_to:" +
|
||||||
new Date().toISOString().split("T")[0],
|
todayLocalDateOnly(),
|
||||||
limit: 1, // Limit to 1 booking, we only need to know if there are any bookings or not
|
limit: 1, // Limit to 1 booking, we only need to know if there are any bookings or not
|
||||||
page: 1,
|
page: 1,
|
||||||
})
|
})
|
||||||
@@ -97,7 +98,7 @@ const emphasis = ref({
|
|||||||
* Check if the department has a daily report for today
|
* Check if the department has a daily report for today
|
||||||
*/
|
*/
|
||||||
SessionUser.request(SessionUser.objects.department_daily_reports.meta.endpoint + "/get", "GET", {
|
SessionUser.request(SessionUser.objects.department_daily_reports.meta.endpoint + "/get", "GET", {
|
||||||
date: new Date().toISOString().split("T")[0],
|
date: todayLocalDateOnly(),
|
||||||
id: department_id, // This refers to the department ID
|
id: department_id, // This refers to the department ID
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { isSearching, searchCustomer, searchCustomerResults } from "@/components/search/economic/customerSearch.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
inputId: {
|
||||||
|
type: String,
|
||||||
|
default: "customer-search-select-input",
|
||||||
|
},
|
||||||
|
testIdPrefix: {
|
||||||
|
type: String,
|
||||||
|
default: "customer-search-select",
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "selected", "cleared"]);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const searchQuery = ref("");
|
||||||
|
const showResults = ref(false);
|
||||||
|
const selectedResultIndex = ref(-1);
|
||||||
|
|
||||||
|
const getCustomerNumber = (customer) => {
|
||||||
|
const parsedValue = Number.parseInt(
|
||||||
|
String(customer?.customerNumber ?? customer?.customer_number ?? customer?.customer_id ?? customer?.id ?? ""),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCustomerName = (customer) => {
|
||||||
|
return String(customer?.name ?? customer?.customer_name ?? customer?.customerName ?? "").trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCustomerCity = (customer) => {
|
||||||
|
return String(customer?.city ?? customer?.address_city ?? "").trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCustomer = (customer) => {
|
||||||
|
if (!customer) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = getCustomerName(customer);
|
||||||
|
const customerNumber = getCustomerNumber(customer);
|
||||||
|
|
||||||
|
return [name, customerNumber ? `#${customerNumber}` : null].filter(Boolean).join(" - ");
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedCustomerNumber = computed(() => getCustomerNumber(props.modelValue));
|
||||||
|
const selectedCustomerName = computed(() => getCustomerName(props.modelValue));
|
||||||
|
const selectedCustomerCity = computed(() => getCustomerCity(props.modelValue));
|
||||||
|
const hasResults = computed(() => searchCustomerResults.value.length > 0);
|
||||||
|
const placeholderText = computed(() => props.placeholder || t("vehicles.add_modal.customer_placeholder"));
|
||||||
|
|
||||||
|
const resetSearchResults = () => {
|
||||||
|
searchCustomer(null);
|
||||||
|
selectedResultIndex.value = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSelectedCustomer = (customer) => {
|
||||||
|
emit("update:modelValue", customer);
|
||||||
|
emit("selected", customer);
|
||||||
|
searchQuery.value = formatCustomer(customer);
|
||||||
|
showResults.value = false;
|
||||||
|
selectedResultIndex.value = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSelectedCustomer = async () => {
|
||||||
|
emit("update:modelValue", null);
|
||||||
|
emit("cleared");
|
||||||
|
searchQuery.value = "";
|
||||||
|
showResults.value = false;
|
||||||
|
resetSearchResults();
|
||||||
|
await nextTick();
|
||||||
|
document.getElementById(props.inputId)?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchInput = () => {
|
||||||
|
if (props.modelValue) {
|
||||||
|
emit("update:modelValue", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = searchQuery.value.trim();
|
||||||
|
selectedResultIndex.value = -1;
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
showResults.value = false;
|
||||||
|
resetSearchResults();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showResults.value = true;
|
||||||
|
searchCustomer(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
if (searchQuery.value.trim() && hasResults.value) {
|
||||||
|
showResults.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
showResults.value = false;
|
||||||
|
selectedResultIndex.value = -1;
|
||||||
|
}, 150);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeydown = (event) => {
|
||||||
|
if (!showResults.value || !hasResults.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "ArrowDown") {
|
||||||
|
event.preventDefault();
|
||||||
|
selectedResultIndex.value = Math.min(selectedResultIndex.value + 1, searchCustomerResults.value.length - 1);
|
||||||
|
} else if (event.key === "ArrowUp") {
|
||||||
|
event.preventDefault();
|
||||||
|
selectedResultIndex.value = Math.max(selectedResultIndex.value - 1, 0);
|
||||||
|
} else if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
const selectedCustomer = searchCustomerResults.value[selectedResultIndex.value] || searchCustomerResults.value[0];
|
||||||
|
if (selectedCustomer) {
|
||||||
|
setSelectedCustomer(selectedCustomer);
|
||||||
|
}
|
||||||
|
} else if (event.key === "Escape") {
|
||||||
|
showResults.value = false;
|
||||||
|
selectedResultIndex.value = -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(customer) => {
|
||||||
|
if (customer) {
|
||||||
|
searchQuery.value = formatCustomer(customer);
|
||||||
|
} else if (!document.activeElement || document.activeElement.id !== props.inputId) {
|
||||||
|
searchQuery.value = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
resetSearchResults();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="customer-search-select">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" :for="inputId">{{ t("vehicles.add_modal.customer_label") }}</label>
|
||||||
|
<div class="control has-icons-left" :class="{ 'is-loading': isSearching }">
|
||||||
|
<input
|
||||||
|
:id="inputId"
|
||||||
|
v-model="searchQuery"
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
:placeholder="placeholderText"
|
||||||
|
:disabled="disabled"
|
||||||
|
:aria-expanded="showResults && hasResults"
|
||||||
|
:data-testid="`${testIdPrefix}-input`"
|
||||||
|
@input="handleSearchInput"
|
||||||
|
@focus="handleFocus"
|
||||||
|
@blur="handleBlur"
|
||||||
|
@keydown="handleKeydown"
|
||||||
|
/>
|
||||||
|
<span class="icon is-left">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="showResults && hasResults"
|
||||||
|
class="dropdown is-active customer-search-select__dropdown"
|
||||||
|
:data-testid="`${testIdPrefix}-results`"
|
||||||
|
>
|
||||||
|
<div class="dropdown-menu customer-search-select__menu" role="listbox">
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<button
|
||||||
|
v-for="(customer, index) in searchCustomerResults"
|
||||||
|
:key="getCustomerNumber(customer) || index"
|
||||||
|
type="button"
|
||||||
|
class="dropdown-item customer-search-select__option"
|
||||||
|
:class="{ 'is-active': selectedResultIndex === index }"
|
||||||
|
:data-testid="`${testIdPrefix}-option-${index}`"
|
||||||
|
@mousedown.prevent="setSelectedCustomer(customer)"
|
||||||
|
>
|
||||||
|
<span class="customer-search-select__option-main">{{ getCustomerName(customer) }}</span>
|
||||||
|
<span class="customer-search-select__option-meta">
|
||||||
|
#{{ getCustomerNumber(customer) }}
|
||||||
|
<template v-if="getCustomerCity(customer)"> · {{ getCustomerCity(customer) }}</template>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="modelValue" class="customer-search-select__selected" :data-testid="`${testIdPrefix}-selected`">
|
||||||
|
<div>
|
||||||
|
<p class="has-text-weight-semibold">{{ t("vehicles.add_modal.selected_customer") }}</p>
|
||||||
|
<p>{{ selectedCustomerName }}</p>
|
||||||
|
<p class="is-size-7 has-text-grey">
|
||||||
|
#{{ selectedCustomerNumber }}
|
||||||
|
<span v-if="selectedCustomerCity"> · {{ selectedCustomerCity }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button is-small is-light"
|
||||||
|
:disabled="disabled"
|
||||||
|
:data-testid="`${testIdPrefix}-clear`"
|
||||||
|
@click="clearSelectedCustomer"
|
||||||
|
>
|
||||||
|
{{ t("common.clear") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.customer-search-select {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-select__dropdown,
|
||||||
|
.customer-search-select__menu {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-select__dropdown {
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 4.75rem;
|
||||||
|
z-index: 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-select__option {
|
||||||
|
align-items: flex-start;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: left;
|
||||||
|
white-space: normal;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-select__option-main {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-select__option-meta {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-select__selected {
|
||||||
|
align-items: flex-start;
|
||||||
|
background: #f5f8fc;
|
||||||
|
border: 1px solid #d8e2ef;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,6 +5,7 @@ import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/Ob
|
|||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import {ref} from "vue";
|
import {ref} from "vue";
|
||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
|
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
const t = (key) => i18n.global.t(key);
|
const t = (key) => i18n.global.t(key);
|
||||||
|
|
||||||
@@ -241,7 +242,7 @@ export const Bookings = {
|
|||||||
'/bookings',
|
'/bookings',
|
||||||
'GET',
|
'GET',
|
||||||
{
|
{
|
||||||
filters: `department:${department},status:pending,date-date_from:${new Date().toISOString().split('T')[0]},date-date_to:${new Date().toISOString().split('T')[0]}`,
|
filters: `department:${department},status:pending,date-date_from:${todayLocalDateOnly()},date-date_to:${todayLocalDateOnly()}`,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
page: 1
|
page: 1
|
||||||
}
|
}
|
||||||
@@ -289,4 +290,4 @@ export const Bookings = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/Ob
|
|||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import {ref} from "vue";
|
import {ref} from "vue";
|
||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
|
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
const t = (key) => i18n.global.t(key);
|
const t = (key) => i18n.global.t(key);
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@ const t = (key) => i18n.global.t(key);
|
|||||||
water_usage_morning: parseInt(water_usage_morning),
|
water_usage_morning: parseInt(water_usage_morning),
|
||||||
water_usage: parseInt(water_usage),
|
water_usage: parseInt(water_usage),
|
||||||
notes: notes ? notes : 'Daily report',
|
notes: notes ? notes : 'Daily report',
|
||||||
date: date ? date : new Date().toISOString().split('T')[0]
|
date: date ? date : todayLocalDateOnly()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
|
import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
import { formatLocalDateOnly, todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The DepartmentTimeBookingsEntries object
|
* The DepartmentTimeBookingsEntries object
|
||||||
@@ -230,10 +231,10 @@ export const DepartmentTimeBookingsEntries = {
|
|||||||
if (!dateFrom) {
|
if (!dateFrom) {
|
||||||
const lastWeek = new Date();
|
const lastWeek = new Date();
|
||||||
lastWeek.setDate(lastWeek.getDate() - 7);
|
lastWeek.setDate(lastWeek.getDate() - 7);
|
||||||
dateFrom = lastWeek.toISOString().split('T')[0]; // Format as YYYY-MM-DD
|
dateFrom = formatLocalDateOnly(lastWeek); // Format as YYYY-MM-DD
|
||||||
}
|
}
|
||||||
if (!dateTo) {
|
if (!dateTo) {
|
||||||
dateTo = new Date().toISOString().split('T')[0]; // Format as YYYY-MM-DD
|
dateTo = todayLocalDateOnly(); // Format as YYYY-MM-DD
|
||||||
}
|
}
|
||||||
console.log("Fetching public available times for department:", departmentId, "from", dateFrom, "to", dateTo);
|
console.log("Fetching public available times for department:", departmentId, "from", dateFrom, "to", dateTo);
|
||||||
// Make the request to fetch public available times for the department
|
// Make the request to fetch public available times for the department
|
||||||
@@ -279,4 +280,4 @@ export const DepartmentTimeBookingsEntries = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3478,17 +3478,17 @@
|
|||||||
"error_report": {
|
"error_report": {
|
||||||
"button": "Rapporter fejl",
|
"button": "Rapporter fejl",
|
||||||
"title": "Rapporter fejl",
|
"title": "Rapporter fejl",
|
||||||
"subtitle": "Send det aktuelle skærmbillede og de seneste fejldetaljer til support.",
|
"subtitle": "Send de seneste fejldetaljer til support. Et skærmbillede vedhæftes, når det er muligt.",
|
||||||
"before_error": "Hvad lavede du, før fejlen opstod?",
|
"before_error": "Hvad lavede du, før fejlen opstod?",
|
||||||
"expected": "Hvad forventede du, der ville ske?",
|
"expected": "Hvad forventede du, der ville ske?",
|
||||||
"actual": "Hvad skete der faktisk?",
|
"actual": "Hvad skete der faktisk?",
|
||||||
"before_error_placeholder": "Beskriv handlingen, du var i gang med, for eksempel at åbne ordrer eller vælge en kunde.",
|
"before_error_placeholder": "Beskriv handlingen, du var i gang med, for eksempel at åbne ordrer eller vælge en kunde.",
|
||||||
"expected_placeholder": "Beskriv det resultat, du forventede at se.",
|
"expected_placeholder": "Beskriv det resultat, du forventede at se.",
|
||||||
"actual_placeholder": "Beskriv, hvad du så i stedet, inklusive eventuel fejltekst.",
|
"actual_placeholder": "Beskriv, hvad du så i stedet, inklusive eventuel fejltekst.",
|
||||||
"consent": "Jeg accepterer, at det aktuelle app-skærmbillede, de seneste request-fejl, Vue-fejl, browseroplysninger og mine svar indsamles til fejlfinding.",
|
"consent": "Jeg accepterer, at de seneste request-fejl, Vue-fejl, browseroplysninger, mine svar og et app-skærmbillede, når det er muligt, indsamles til fejlfinding.",
|
||||||
"submit": "Send rapport",
|
"submit": "Send rapport",
|
||||||
"submitted": "Fejlrapporten er sendt.",
|
"submitted": "Fejlrapporten er sendt.",
|
||||||
"capture_failed": "Skærmbilledet kunne ikke oprettes. Prøv igen.",
|
"capture_failed": "Skærmbilledet kunne ikke oprettes. Rapporten sendes uden skærmbillede.",
|
||||||
"submit_failed": "Fejlrapporten kunne ikke sendes.",
|
"submit_failed": "Fejlrapporten kunne ikke sendes.",
|
||||||
"required": "Alle felter og accept af dataindsamling er påkrævet.",
|
"required": "Alle felter og accept af dataindsamling er påkrævet.",
|
||||||
"page_subtitle": "Rapporterede skærme, request-fejl og Vue-fejl",
|
"page_subtitle": "Rapporterede skærme, request-fejl og Vue-fejl",
|
||||||
@@ -5932,6 +5932,20 @@
|
|||||||
},
|
},
|
||||||
"vehicles": {
|
"vehicles": {
|
||||||
"add": "@:{'words.generated.tilføj'} @:{'words.generated.køretøj'}",
|
"add": "@:{'words.generated.tilføj'} @:{'words.generated.køretøj'}",
|
||||||
|
"add_modal": {
|
||||||
|
"customer_label": "Kunde",
|
||||||
|
"customer_placeholder": "Søg efter kundenavn eller kundenummer",
|
||||||
|
"error": "Køretøjet kunne ikke tilføjes.",
|
||||||
|
"no_customer_results": "Ingen kunder fundet",
|
||||||
|
"reference_placeholder": "Valgfri reference",
|
||||||
|
"registration_placeholder": "Registreringsnummer",
|
||||||
|
"selected_customer": "Valgt kunde",
|
||||||
|
"submit": "Tilføj køretøj",
|
||||||
|
"title": "Tilføj køretøj",
|
||||||
|
"type_load_error": "Køretøjstyper kunne ikke indlæses.",
|
||||||
|
"type_placeholder": "Vælg køretøjstype",
|
||||||
|
"validation_error": "Vælg kunde, registreringsnummer og køretøjstype."
|
||||||
|
},
|
||||||
"brand": "@:{'templates.generated.compat.admin.pos.make'}",
|
"brand": "@:{'templates.generated.compat.admin.pos.make'}",
|
||||||
"color": "Farve",
|
"color": "Farve",
|
||||||
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
|
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
|
||||||
|
|||||||
@@ -3589,17 +3589,17 @@
|
|||||||
"error_report": {
|
"error_report": {
|
||||||
"button": "Fehler melden",
|
"button": "Fehler melden",
|
||||||
"title": "Fehler melden",
|
"title": "Fehler melden",
|
||||||
"subtitle": "Sende den aktuellen Bildschirm und die letzten Fehlerdetails an den Support.",
|
"subtitle": "Sende die letzten Fehlerdetails an den Support. Ein Screenshot wird angehängt, wenn möglich.",
|
||||||
"before_error": "Was haben Sie getan, bevor der Fehler auftrat?",
|
"before_error": "Was haben Sie getan, bevor der Fehler auftrat?",
|
||||||
"expected": "Was hatten Sie erwartet?",
|
"expected": "Was hatten Sie erwartet?",
|
||||||
"actual": "Was ist tatsächlich passiert?",
|
"actual": "Was ist tatsächlich passiert?",
|
||||||
"before_error_placeholder": "Beschreiben Sie die Aktion, zum Beispiel Bestellungen öffnen oder einen Kunden auswählen.",
|
"before_error_placeholder": "Beschreiben Sie die Aktion, zum Beispiel Bestellungen öffnen oder einen Kunden auswählen.",
|
||||||
"expected_placeholder": "Beschreiben Sie das erwartete Ergebnis.",
|
"expected_placeholder": "Beschreiben Sie das erwartete Ergebnis.",
|
||||||
"actual_placeholder": "Beschreiben Sie, was stattdessen zu sehen war, inklusive Fehlermeldung.",
|
"actual_placeholder": "Beschreiben Sie, was stattdessen zu sehen war, inklusive Fehlermeldung.",
|
||||||
"consent": "Ich akzeptiere, dass der aktuelle App-Bildschirm, letzte Request-Fehler, Vue-Fehler, Browserdetails und meine Antworten zur Fehlersuche erfasst werden.",
|
"consent": "Ich akzeptiere, dass letzte Request-Fehler, Vue-Fehler, Browserdetails, meine Antworten und ein App-Screenshot, wenn möglich, zur Fehlersuche erfasst werden.",
|
||||||
"submit": "Bericht senden",
|
"submit": "Bericht senden",
|
||||||
"submitted": "Fehlerbericht gesendet.",
|
"submitted": "Fehlerbericht gesendet.",
|
||||||
"capture_failed": "Der Screenshot konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
|
"capture_failed": "Der Screenshot konnte nicht erstellt werden. Der Bericht wird ohne Screenshot gesendet.",
|
||||||
"submit_failed": "Der Fehlerbericht konnte nicht gesendet werden.",
|
"submit_failed": "Der Fehlerbericht konnte nicht gesendet werden.",
|
||||||
"required": "Alle Felder und die Zustimmung zur Datenerfassung sind erforderlich.",
|
"required": "Alle Felder und die Zustimmung zur Datenerfassung sind erforderlich.",
|
||||||
"page_subtitle": "Gemeldete Bildschirme, Request-Fehler und Vue-Fehler",
|
"page_subtitle": "Gemeldete Bildschirme, Request-Fehler und Vue-Fehler",
|
||||||
|
|||||||
@@ -3310,17 +3310,17 @@
|
|||||||
"error_report": {
|
"error_report": {
|
||||||
"button": "Report error",
|
"button": "Report error",
|
||||||
"title": "Report error",
|
"title": "Report error",
|
||||||
"subtitle": "Send the current screen and recent error details to support.",
|
"subtitle": "Send recent error details to support. A screenshot is attached when available.",
|
||||||
"before_error": "What were you doing before the error occurred?",
|
"before_error": "What were you doing before the error occurred?",
|
||||||
"expected": "What did you expect would happen?",
|
"expected": "What did you expect would happen?",
|
||||||
"actual": "What actually happened?",
|
"actual": "What actually happened?",
|
||||||
"before_error_placeholder": "Describe the action you were taking, for example opening orders or selecting a customer.",
|
"before_error_placeholder": "Describe the action you were taking, for example opening orders or selecting a customer.",
|
||||||
"expected_placeholder": "Describe the result you expected to see.",
|
"expected_placeholder": "Describe the result you expected to see.",
|
||||||
"actual_placeholder": "Describe what you saw instead, including any error text.",
|
"actual_placeholder": "Describe what you saw instead, including any error text.",
|
||||||
"consent": "I accept that the current app screen, recent request errors, Vue errors, browser details, and my answers are collected for troubleshooting.",
|
"consent": "I accept that recent request errors, Vue errors, browser details, my answers, and an app screenshot when available are collected for troubleshooting.",
|
||||||
"submit": "Submit report",
|
"submit": "Submit report",
|
||||||
"submitted": "Error report submitted.",
|
"submitted": "Error report submitted.",
|
||||||
"capture_failed": "The screen capture failed. Please try again.",
|
"capture_failed": "The screen capture failed. The report will be sent without a screenshot.",
|
||||||
"submit_failed": "The error report could not be submitted.",
|
"submit_failed": "The error report could not be submitted.",
|
||||||
"required": "All fields and data collection acceptance are required.",
|
"required": "All fields and data collection acceptance are required.",
|
||||||
"page_subtitle": "Reported screens, request failures, and Vue errors",
|
"page_subtitle": "Reported screens, request failures, and Vue errors",
|
||||||
@@ -5764,6 +5764,20 @@
|
|||||||
},
|
},
|
||||||
"vehicles": {
|
"vehicles": {
|
||||||
"add": "@.capitalize:{'words.generated.add'} @:{'words.generated.vehicle'}",
|
"add": "@.capitalize:{'words.generated.add'} @:{'words.generated.vehicle'}",
|
||||||
|
"add_modal": {
|
||||||
|
"customer_label": "Customer",
|
||||||
|
"customer_placeholder": "Search by customer name or number",
|
||||||
|
"error": "Unable to add vehicle.",
|
||||||
|
"no_customer_results": "No customers found",
|
||||||
|
"reference_placeholder": "Optional reference",
|
||||||
|
"registration_placeholder": "Registration number",
|
||||||
|
"selected_customer": "Selected customer",
|
||||||
|
"submit": "Add vehicle",
|
||||||
|
"title": "Add vehicle",
|
||||||
|
"type_load_error": "Unable to load vehicle types.",
|
||||||
|
"type_placeholder": "Select vehicle type",
|
||||||
|
"validation_error": "Select a customer, registration number, and vehicle type."
|
||||||
|
},
|
||||||
"brand": "Brand",
|
"brand": "Brand",
|
||||||
"color": "Color",
|
"color": "Color",
|
||||||
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
|
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
|
||||||
|
|||||||
@@ -5834,6 +5834,20 @@
|
|||||||
"vehicles": {
|
"vehicles": {
|
||||||
"actions": "@:common.actions",
|
"actions": "@:common.actions",
|
||||||
"add": "@:{'templates.generated.compat.vehicles.add'}",
|
"add": "@:{'templates.generated.compat.vehicles.add'}",
|
||||||
|
"add_modal": {
|
||||||
|
"customer_label": "@:{'templates.generated.compat.vehicles.add_modal.customer_label'}",
|
||||||
|
"customer_placeholder": "@:{'templates.generated.compat.vehicles.add_modal.customer_placeholder'}",
|
||||||
|
"error": "@:{'templates.generated.compat.vehicles.add_modal.error'}",
|
||||||
|
"no_customer_results": "@:{'templates.generated.compat.vehicles.add_modal.no_customer_results'}",
|
||||||
|
"reference_placeholder": "@:{'templates.generated.compat.vehicles.add_modal.reference_placeholder'}",
|
||||||
|
"registration_placeholder": "@:{'templates.generated.compat.vehicles.add_modal.registration_placeholder'}",
|
||||||
|
"selected_customer": "@:{'templates.generated.compat.vehicles.add_modal.selected_customer'}",
|
||||||
|
"submit": "@:{'templates.generated.compat.vehicles.add_modal.submit'}",
|
||||||
|
"title": "@:{'templates.generated.compat.vehicles.add_modal.title'}",
|
||||||
|
"type_load_error": "@:{'templates.generated.compat.vehicles.add_modal.type_load_error'}",
|
||||||
|
"type_placeholder": "@:{'templates.generated.compat.vehicles.add_modal.type_placeholder'}",
|
||||||
|
"validation_error": "@:{'templates.generated.compat.vehicles.add_modal.validation_error'}"
|
||||||
|
},
|
||||||
"brand": "@:{'templates.generated.compat.vehicles.brand'}",
|
"brand": "@:{'templates.generated.compat.vehicles.brand'}",
|
||||||
"color": "@:{'templates.generated.compat.vehicles.color'}",
|
"color": "@:{'templates.generated.compat.vehicles.color'}",
|
||||||
"created_at": "@:{'templates.generated.compat.global.generated'}",
|
"created_at": "@:{'templates.generated.compat.global.generated'}",
|
||||||
|
|||||||
@@ -3592,17 +3592,17 @@
|
|||||||
"error_report": {
|
"error_report": {
|
||||||
"button": "Rapporter feil",
|
"button": "Rapporter feil",
|
||||||
"title": "Rapporter feil",
|
"title": "Rapporter feil",
|
||||||
"subtitle": "Send gjeldende skjermbilde og de siste feildetaljene til support.",
|
"subtitle": "Send de siste feildetaljene til support. Et skjermbilde legges ved når det er mulig.",
|
||||||
"before_error": "Hva gjorde du før feilen oppstod?",
|
"before_error": "Hva gjorde du før feilen oppstod?",
|
||||||
"expected": "Hva forventet du at skulle skje?",
|
"expected": "Hva forventet du at skulle skje?",
|
||||||
"actual": "Hva skjedde faktisk?",
|
"actual": "Hva skjedde faktisk?",
|
||||||
"before_error_placeholder": "Beskriv handlingen du utførte, for eksempel å åpne ordre eller velge en kunde.",
|
"before_error_placeholder": "Beskriv handlingen du utførte, for eksempel å åpne ordre eller velge en kunde.",
|
||||||
"expected_placeholder": "Beskriv resultatet du forventet å se.",
|
"expected_placeholder": "Beskriv resultatet du forventet å se.",
|
||||||
"actual_placeholder": "Beskriv hva du så i stedet, inkludert eventuell feiltekst.",
|
"actual_placeholder": "Beskriv hva du så i stedet, inkludert eventuell feiltekst.",
|
||||||
"consent": "Jeg godtar at gjeldende app-skjermbilde, siste request-feil, Vue-feil, nettleserdetaljer og svarene mine samles inn for feilsøking.",
|
"consent": "Jeg godtar at siste request-feil, Vue-feil, nettleserdetaljer, svarene mine og et app-skjermbilde når det er mulig, samles inn for feilsøking.",
|
||||||
"submit": "Send rapport",
|
"submit": "Send rapport",
|
||||||
"submitted": "Feilrapport sendt.",
|
"submitted": "Feilrapport sendt.",
|
||||||
"capture_failed": "Skjermbildet kunne ikke tas. Prøv igjen.",
|
"capture_failed": "Skjermbildet kunne ikke tas. Rapporten sendes uten skjermbilde.",
|
||||||
"submit_failed": "Feilrapporten kunne ikke sendes.",
|
"submit_failed": "Feilrapporten kunne ikke sendes.",
|
||||||
"required": "Alle felter og godkjenning av datainnsamling er påkrevd.",
|
"required": "Alle felter og godkjenning av datainnsamling er påkrevd.",
|
||||||
"page_subtitle": "Rapporterte skjermer, request-feil og Vue-feil",
|
"page_subtitle": "Rapporterte skjermer, request-feil og Vue-feil",
|
||||||
|
|||||||
@@ -3642,17 +3642,17 @@
|
|||||||
"error_report": {
|
"error_report": {
|
||||||
"button": "Rapportera fel",
|
"button": "Rapportera fel",
|
||||||
"title": "Rapportera fel",
|
"title": "Rapportera fel",
|
||||||
"subtitle": "Skicka aktuell skärm och de senaste feldetaljerna till support.",
|
"subtitle": "Skicka de senaste feldetaljerna till support. En skärmbild bifogas när det är möjligt.",
|
||||||
"before_error": "Vad gjorde du innan felet uppstod?",
|
"before_error": "Vad gjorde du innan felet uppstod?",
|
||||||
"expected": "Vad förväntade du dig skulle hända?",
|
"expected": "Vad förväntade du dig skulle hända?",
|
||||||
"actual": "Vad hände faktiskt?",
|
"actual": "Vad hände faktiskt?",
|
||||||
"before_error_placeholder": "Beskriv åtgärden du gjorde, till exempel att öppna ordrar eller välja en kund.",
|
"before_error_placeholder": "Beskriv åtgärden du gjorde, till exempel att öppna ordrar eller välja en kund.",
|
||||||
"expected_placeholder": "Beskriv resultatet du förväntade dig att se.",
|
"expected_placeholder": "Beskriv resultatet du förväntade dig att se.",
|
||||||
"actual_placeholder": "Beskriv vad du såg i stället, inklusive eventuell feltext.",
|
"actual_placeholder": "Beskriv vad du såg i stället, inklusive eventuell feltext.",
|
||||||
"consent": "Jag accepterar att aktuell app-skärm, senaste request-fel, Vue-fel, webbläsardetaljer och mina svar samlas in för felsökning.",
|
"consent": "Jag accepterar att senaste request-fel, Vue-fel, webbläsardetaljer, mina svar och en app-skärmbild när det är möjligt samlas in för felsökning.",
|
||||||
"submit": "Skicka rapport",
|
"submit": "Skicka rapport",
|
||||||
"submitted": "Felrapport skickad.",
|
"submitted": "Felrapport skickad.",
|
||||||
"capture_failed": "Skärmbilden kunde inte tas. Försök igen.",
|
"capture_failed": "Skärmbilden kunde inte tas. Rapporten skickas utan skärmbild.",
|
||||||
"submit_failed": "Felrapporten kunde inte skickas.",
|
"submit_failed": "Felrapporten kunde inte skickas.",
|
||||||
"required": "Alla fält och godkännande av datainsamling krävs.",
|
"required": "Alla fält och godkännande av datainsamling krävs.",
|
||||||
"page_subtitle": "Rapporterade skärmar, request-fel och Vue-fel",
|
"page_subtitle": "Rapporterade skärmar, request-fel och Vue-fel",
|
||||||
|
|||||||
@@ -3,17 +3,17 @@
|
|||||||
"error_report": {
|
"error_report": {
|
||||||
"button": "Rapporter fejl",
|
"button": "Rapporter fejl",
|
||||||
"title": "Rapporter fejl",
|
"title": "Rapporter fejl",
|
||||||
"subtitle": "Send det aktuelle skærmbillede og de seneste fejldetaljer til support.",
|
"subtitle": "Send de seneste fejldetaljer til support. Et skærmbillede vedhæftes, når det er muligt.",
|
||||||
"before_error": "Hvad lavede du, før fejlen opstod?",
|
"before_error": "Hvad lavede du, før fejlen opstod?",
|
||||||
"expected": "Hvad forventede du, der ville ske?",
|
"expected": "Hvad forventede du, der ville ske?",
|
||||||
"actual": "Hvad skete der faktisk?",
|
"actual": "Hvad skete der faktisk?",
|
||||||
"before_error_placeholder": "Beskriv handlingen, du var i gang med, for eksempel at åbne ordrer eller vælge en kunde.",
|
"before_error_placeholder": "Beskriv handlingen, du var i gang med, for eksempel at åbne ordrer eller vælge en kunde.",
|
||||||
"expected_placeholder": "Beskriv det resultat, du forventede at se.",
|
"expected_placeholder": "Beskriv det resultat, du forventede at se.",
|
||||||
"actual_placeholder": "Beskriv, hvad du så i stedet, inklusive eventuel fejltekst.",
|
"actual_placeholder": "Beskriv, hvad du så i stedet, inklusive eventuel fejltekst.",
|
||||||
"consent": "Jeg accepterer, at det aktuelle app-skærmbillede, de seneste request-fejl, Vue-fejl, browseroplysninger og mine svar indsamles til fejlfinding.",
|
"consent": "Jeg accepterer, at de seneste request-fejl, Vue-fejl, browseroplysninger, mine svar og et app-skærmbillede, når det er muligt, indsamles til fejlfinding.",
|
||||||
"submit": "Send rapport",
|
"submit": "Send rapport",
|
||||||
"submitted": "Fejlrapporten er sendt.",
|
"submitted": "Fejlrapporten er sendt.",
|
||||||
"capture_failed": "Skærmbilledet kunne ikke oprettes. Prøv igen.",
|
"capture_failed": "Skærmbilledet kunne ikke oprettes. Rapporten sendes uden skærmbillede.",
|
||||||
"submit_failed": "Fejlrapporten kunne ikke sendes.",
|
"submit_failed": "Fejlrapporten kunne ikke sendes.",
|
||||||
"required": "Alle felter og accept af dataindsamling er påkrævet.",
|
"required": "Alle felter og accept af dataindsamling er påkrævet.",
|
||||||
"page_subtitle": "Rapporterede skærme, request-fejl og Vue-fejl",
|
"page_subtitle": "Rapporterede skærme, request-fejl og Vue-fejl",
|
||||||
|
|||||||
@@ -2,6 +2,20 @@
|
|||||||
"compat": {
|
"compat": {
|
||||||
"vehicles": {
|
"vehicles": {
|
||||||
"add": "@:{'terms.glossary.tilføj'} @:{'terms.glossary.køretøj'}",
|
"add": "@:{'terms.glossary.tilføj'} @:{'terms.glossary.køretøj'}",
|
||||||
|
"add_modal": {
|
||||||
|
"customer_label": "Kunde",
|
||||||
|
"customer_placeholder": "Søg efter kundenavn eller kundenummer",
|
||||||
|
"error": "Køretøjet kunne ikke tilføjes.",
|
||||||
|
"no_customer_results": "Ingen kunder fundet",
|
||||||
|
"reference_placeholder": "Valgfri reference",
|
||||||
|
"registration_placeholder": "Registreringsnummer",
|
||||||
|
"selected_customer": "Valgt kunde",
|
||||||
|
"submit": "Tilføj køretøj",
|
||||||
|
"title": "Tilføj køretøj",
|
||||||
|
"type_load_error": "Køretøjstyper kunne ikke indlæses.",
|
||||||
|
"type_placeholder": "Vælg køretøjstype",
|
||||||
|
"validation_error": "Vælg kunde, registreringsnummer og køretøjstype."
|
||||||
|
},
|
||||||
"brand": "@:{'phrases.compat.admin.pos.make'}",
|
"brand": "@:{'phrases.compat.admin.pos.make'}",
|
||||||
"color": "Farve",
|
"color": "Farve",
|
||||||
"delete_vehicle": "@:{'phrases.compat.vehicles.delete'}",
|
"delete_vehicle": "@:{'phrases.compat.vehicles.delete'}",
|
||||||
|
|||||||
@@ -3,17 +3,17 @@
|
|||||||
"error_report": {
|
"error_report": {
|
||||||
"button": "Fehler melden",
|
"button": "Fehler melden",
|
||||||
"title": "Fehler melden",
|
"title": "Fehler melden",
|
||||||
"subtitle": "Sende den aktuellen Bildschirm und die letzten Fehlerdetails an den Support.",
|
"subtitle": "Sende die letzten Fehlerdetails an den Support. Ein Screenshot wird angehängt, wenn möglich.",
|
||||||
"before_error": "Was haben Sie getan, bevor der Fehler auftrat?",
|
"before_error": "Was haben Sie getan, bevor der Fehler auftrat?",
|
||||||
"expected": "Was hatten Sie erwartet?",
|
"expected": "Was hatten Sie erwartet?",
|
||||||
"actual": "Was ist tatsächlich passiert?",
|
"actual": "Was ist tatsächlich passiert?",
|
||||||
"before_error_placeholder": "Beschreiben Sie die Aktion, zum Beispiel Bestellungen öffnen oder einen Kunden auswählen.",
|
"before_error_placeholder": "Beschreiben Sie die Aktion, zum Beispiel Bestellungen öffnen oder einen Kunden auswählen.",
|
||||||
"expected_placeholder": "Beschreiben Sie das erwartete Ergebnis.",
|
"expected_placeholder": "Beschreiben Sie das erwartete Ergebnis.",
|
||||||
"actual_placeholder": "Beschreiben Sie, was stattdessen zu sehen war, inklusive Fehlermeldung.",
|
"actual_placeholder": "Beschreiben Sie, was stattdessen zu sehen war, inklusive Fehlermeldung.",
|
||||||
"consent": "Ich akzeptiere, dass der aktuelle App-Bildschirm, letzte Request-Fehler, Vue-Fehler, Browserdetails und meine Antworten zur Fehlersuche erfasst werden.",
|
"consent": "Ich akzeptiere, dass letzte Request-Fehler, Vue-Fehler, Browserdetails, meine Antworten und ein App-Screenshot, wenn möglich, zur Fehlersuche erfasst werden.",
|
||||||
"submit": "Bericht senden",
|
"submit": "Bericht senden",
|
||||||
"submitted": "Fehlerbericht gesendet.",
|
"submitted": "Fehlerbericht gesendet.",
|
||||||
"capture_failed": "Der Screenshot konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
|
"capture_failed": "Der Screenshot konnte nicht erstellt werden. Der Bericht wird ohne Screenshot gesendet.",
|
||||||
"submit_failed": "Der Fehlerbericht konnte nicht gesendet werden.",
|
"submit_failed": "Der Fehlerbericht konnte nicht gesendet werden.",
|
||||||
"required": "Alle Felder und die Zustimmung zur Datenerfassung sind erforderlich.",
|
"required": "Alle Felder und die Zustimmung zur Datenerfassung sind erforderlich.",
|
||||||
"page_subtitle": "Gemeldete Bildschirme, Request-Fehler und Vue-Fehler",
|
"page_subtitle": "Gemeldete Bildschirme, Request-Fehler und Vue-Fehler",
|
||||||
|
|||||||
@@ -3,17 +3,17 @@
|
|||||||
"error_report": {
|
"error_report": {
|
||||||
"button": "Report error",
|
"button": "Report error",
|
||||||
"title": "Report error",
|
"title": "Report error",
|
||||||
"subtitle": "Send the current screen and recent error details to support.",
|
"subtitle": "Send recent error details to support. A screenshot is attached when available.",
|
||||||
"before_error": "What were you doing before the error occurred?",
|
"before_error": "What were you doing before the error occurred?",
|
||||||
"expected": "What did you expect would happen?",
|
"expected": "What did you expect would happen?",
|
||||||
"actual": "What actually happened?",
|
"actual": "What actually happened?",
|
||||||
"before_error_placeholder": "Describe the action you were taking, for example opening orders or selecting a customer.",
|
"before_error_placeholder": "Describe the action you were taking, for example opening orders or selecting a customer.",
|
||||||
"expected_placeholder": "Describe the result you expected to see.",
|
"expected_placeholder": "Describe the result you expected to see.",
|
||||||
"actual_placeholder": "Describe what you saw instead, including any error text.",
|
"actual_placeholder": "Describe what you saw instead, including any error text.",
|
||||||
"consent": "I accept that the current app screen, recent request errors, Vue errors, browser details, and my answers are collected for troubleshooting.",
|
"consent": "I accept that recent request errors, Vue errors, browser details, my answers, and an app screenshot when available are collected for troubleshooting.",
|
||||||
"submit": "Submit report",
|
"submit": "Submit report",
|
||||||
"submitted": "Error report submitted.",
|
"submitted": "Error report submitted.",
|
||||||
"capture_failed": "The screen capture failed. Please try again.",
|
"capture_failed": "The screen capture failed. The report will be sent without a screenshot.",
|
||||||
"submit_failed": "The error report could not be submitted.",
|
"submit_failed": "The error report could not be submitted.",
|
||||||
"required": "All fields and data collection acceptance are required.",
|
"required": "All fields and data collection acceptance are required.",
|
||||||
"page_subtitle": "Reported screens, request failures, and Vue errors",
|
"page_subtitle": "Reported screens, request failures, and Vue errors",
|
||||||
|
|||||||
@@ -2,6 +2,20 @@
|
|||||||
"compat": {
|
"compat": {
|
||||||
"vehicles": {
|
"vehicles": {
|
||||||
"add": "@.capitalize:{'terms.glossary.add'} @:{'terms.glossary.vehicle'}",
|
"add": "@.capitalize:{'terms.glossary.add'} @:{'terms.glossary.vehicle'}",
|
||||||
|
"add_modal": {
|
||||||
|
"customer_label": "Customer",
|
||||||
|
"customer_placeholder": "Search by customer name or number",
|
||||||
|
"error": "Unable to add vehicle.",
|
||||||
|
"no_customer_results": "No customers found",
|
||||||
|
"reference_placeholder": "Optional reference",
|
||||||
|
"registration_placeholder": "Registration number",
|
||||||
|
"selected_customer": "Selected customer",
|
||||||
|
"submit": "Add vehicle",
|
||||||
|
"title": "Add vehicle",
|
||||||
|
"type_load_error": "Unable to load vehicle types.",
|
||||||
|
"type_placeholder": "Select vehicle type",
|
||||||
|
"validation_error": "Select a customer, registration number, and vehicle type."
|
||||||
|
},
|
||||||
"brand": "Brand",
|
"brand": "Brand",
|
||||||
"color": "Color",
|
"color": "Color",
|
||||||
"delete_vehicle": "@:{'phrases.compat.vehicles.delete'}",
|
"delete_vehicle": "@:{'phrases.compat.vehicles.delete'}",
|
||||||
|
|||||||
@@ -2,6 +2,20 @@
|
|||||||
"vehicles": {
|
"vehicles": {
|
||||||
"actions": "@:common.actions",
|
"actions": "@:common.actions",
|
||||||
"add": "@:{'phrases.compat.vehicles.add'}",
|
"add": "@:{'phrases.compat.vehicles.add'}",
|
||||||
|
"add_modal": {
|
||||||
|
"customer_label": "@:{'phrases.compat.vehicles.add_modal.customer_label'}",
|
||||||
|
"customer_placeholder": "@:{'phrases.compat.vehicles.add_modal.customer_placeholder'}",
|
||||||
|
"error": "@:{'phrases.compat.vehicles.add_modal.error'}",
|
||||||
|
"no_customer_results": "@:{'phrases.compat.vehicles.add_modal.no_customer_results'}",
|
||||||
|
"reference_placeholder": "@:{'phrases.compat.vehicles.add_modal.reference_placeholder'}",
|
||||||
|
"registration_placeholder": "@:{'phrases.compat.vehicles.add_modal.registration_placeholder'}",
|
||||||
|
"selected_customer": "@:{'phrases.compat.vehicles.add_modal.selected_customer'}",
|
||||||
|
"submit": "@:{'phrases.compat.vehicles.add_modal.submit'}",
|
||||||
|
"title": "@:{'phrases.compat.vehicles.add_modal.title'}",
|
||||||
|
"type_load_error": "@:{'phrases.compat.vehicles.add_modal.type_load_error'}",
|
||||||
|
"type_placeholder": "@:{'phrases.compat.vehicles.add_modal.type_placeholder'}",
|
||||||
|
"validation_error": "@:{'phrases.compat.vehicles.add_modal.validation_error'}"
|
||||||
|
},
|
||||||
"brand": "@:{'phrases.compat.vehicles.brand'}",
|
"brand": "@:{'phrases.compat.vehicles.brand'}",
|
||||||
"color": "@:{'phrases.compat.vehicles.color'}",
|
"color": "@:{'phrases.compat.vehicles.color'}",
|
||||||
"created_at": "@:{'phrases.compat.global.generated'}",
|
"created_at": "@:{'phrases.compat.global.generated'}",
|
||||||
|
|||||||
@@ -3,17 +3,17 @@
|
|||||||
"error_report": {
|
"error_report": {
|
||||||
"button": "Rapporter feil",
|
"button": "Rapporter feil",
|
||||||
"title": "Rapporter feil",
|
"title": "Rapporter feil",
|
||||||
"subtitle": "Send gjeldende skjermbilde og de siste feildetaljene til support.",
|
"subtitle": "Send de siste feildetaljene til support. Et skjermbilde legges ved når det er mulig.",
|
||||||
"before_error": "Hva gjorde du før feilen oppstod?",
|
"before_error": "Hva gjorde du før feilen oppstod?",
|
||||||
"expected": "Hva forventet du at skulle skje?",
|
"expected": "Hva forventet du at skulle skje?",
|
||||||
"actual": "Hva skjedde faktisk?",
|
"actual": "Hva skjedde faktisk?",
|
||||||
"before_error_placeholder": "Beskriv handlingen du utførte, for eksempel å åpne ordre eller velge en kunde.",
|
"before_error_placeholder": "Beskriv handlingen du utførte, for eksempel å åpne ordre eller velge en kunde.",
|
||||||
"expected_placeholder": "Beskriv resultatet du forventet å se.",
|
"expected_placeholder": "Beskriv resultatet du forventet å se.",
|
||||||
"actual_placeholder": "Beskriv hva du så i stedet, inkludert eventuell feiltekst.",
|
"actual_placeholder": "Beskriv hva du så i stedet, inkludert eventuell feiltekst.",
|
||||||
"consent": "Jeg godtar at gjeldende app-skjermbilde, siste request-feil, Vue-feil, nettleserdetaljer og svarene mine samles inn for feilsøking.",
|
"consent": "Jeg godtar at siste request-feil, Vue-feil, nettleserdetaljer, svarene mine og et app-skjermbilde når det er mulig, samles inn for feilsøking.",
|
||||||
"submit": "Send rapport",
|
"submit": "Send rapport",
|
||||||
"submitted": "Feilrapport sendt.",
|
"submitted": "Feilrapport sendt.",
|
||||||
"capture_failed": "Skjermbildet kunne ikke tas. Prøv igjen.",
|
"capture_failed": "Skjermbildet kunne ikke tas. Rapporten sendes uten skjermbilde.",
|
||||||
"submit_failed": "Feilrapporten kunne ikke sendes.",
|
"submit_failed": "Feilrapporten kunne ikke sendes.",
|
||||||
"required": "Alle felter og godkjenning av datainnsamling er påkrevd.",
|
"required": "Alle felter og godkjenning av datainnsamling er påkrevd.",
|
||||||
"page_subtitle": "Rapporterte skjermer, request-feil og Vue-feil",
|
"page_subtitle": "Rapporterte skjermer, request-feil og Vue-feil",
|
||||||
|
|||||||
@@ -3,17 +3,17 @@
|
|||||||
"error_report": {
|
"error_report": {
|
||||||
"button": "Rapportera fel",
|
"button": "Rapportera fel",
|
||||||
"title": "Rapportera fel",
|
"title": "Rapportera fel",
|
||||||
"subtitle": "Skicka aktuell skärm och de senaste feldetaljerna till support.",
|
"subtitle": "Skicka de senaste feldetaljerna till support. En skärmbild bifogas när det är möjligt.",
|
||||||
"before_error": "Vad gjorde du innan felet uppstod?",
|
"before_error": "Vad gjorde du innan felet uppstod?",
|
||||||
"expected": "Vad förväntade du dig skulle hända?",
|
"expected": "Vad förväntade du dig skulle hända?",
|
||||||
"actual": "Vad hände faktiskt?",
|
"actual": "Vad hände faktiskt?",
|
||||||
"before_error_placeholder": "Beskriv åtgärden du gjorde, till exempel att öppna ordrar eller välja en kund.",
|
"before_error_placeholder": "Beskriv åtgärden du gjorde, till exempel att öppna ordrar eller välja en kund.",
|
||||||
"expected_placeholder": "Beskriv resultatet du förväntade dig att se.",
|
"expected_placeholder": "Beskriv resultatet du förväntade dig att se.",
|
||||||
"actual_placeholder": "Beskriv vad du såg i stället, inklusive eventuell feltext.",
|
"actual_placeholder": "Beskriv vad du såg i stället, inklusive eventuell feltext.",
|
||||||
"consent": "Jag accepterar att aktuell app-skärm, senaste request-fel, Vue-fel, webbläsardetaljer och mina svar samlas in för felsökning.",
|
"consent": "Jag accepterar att senaste request-fel, Vue-fel, webbläsardetaljer, mina svar och en app-skärmbild när det är möjligt samlas in för felsökning.",
|
||||||
"submit": "Skicka rapport",
|
"submit": "Skicka rapport",
|
||||||
"submitted": "Felrapport skickad.",
|
"submitted": "Felrapport skickad.",
|
||||||
"capture_failed": "Skärmbilden kunde inte tas. Försök igen.",
|
"capture_failed": "Skärmbilden kunde inte tas. Rapporten skickas utan skärmbild.",
|
||||||
"submit_failed": "Felrapporten kunde inte skickas.",
|
"submit_failed": "Felrapporten kunde inte skickas.",
|
||||||
"required": "Alla fält och godkännande av datainsamling krävs.",
|
"required": "Alla fält och godkännande av datainsamling krävs.",
|
||||||
"page_subtitle": "Rapporterade skärmar, request-fel och Vue-fel",
|
"page_subtitle": "Rapporterade skärmar, request-fel och Vue-fel",
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
||||||
|
const DATE_ONLY_PREFIX_PATTERN = /^(\d{4}-\d{2}-\d{2})/;
|
||||||
|
|
||||||
|
const padDatePart = (value) => String(value).padStart(2, "0");
|
||||||
|
|
||||||
|
export const isValidDate = (value) => value instanceof Date && !Number.isNaN(value.getTime());
|
||||||
|
|
||||||
|
export const formatLocalDateOnly = (value = new Date()) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const directMatch = value.trim().match(DATE_ONLY_PREFIX_PATTERN);
|
||||||
|
if (directMatch) {
|
||||||
|
return directMatch[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = value instanceof Date ? value : new Date(value);
|
||||||
|
if (!isValidDate(date)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
date.getFullYear(),
|
||||||
|
padDatePart(date.getMonth() + 1),
|
||||||
|
padDatePart(date.getDate()),
|
||||||
|
].join("-");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseLocalDateOnly = (value) => {
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return isValidDate(value)
|
||||||
|
? new Date(value.getFullYear(), value.getMonth(), value.getDate())
|
||||||
|
: new Date(Number.NaN);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringValue = String(value ?? "").trim();
|
||||||
|
const match = DATE_ONLY_PATTERN.exec(stringValue);
|
||||||
|
if (match) {
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
const day = Number(match[3]);
|
||||||
|
const parsed = new Date(year, month - 1, day);
|
||||||
|
|
||||||
|
if (
|
||||||
|
parsed.getFullYear() === year
|
||||||
|
&& parsed.getMonth() === month - 1
|
||||||
|
&& parsed.getDate() === day
|
||||||
|
) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(Number.NaN);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = new Date(stringValue);
|
||||||
|
return isValidDate(parsed)
|
||||||
|
? new Date(parsed.getFullYear(), parsed.getMonth(), parsed.getDate())
|
||||||
|
: parsed;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const todayLocalDateOnly = () => formatLocalDateOnly(new Date());
|
||||||
|
|
||||||
|
export const yesterdayLocalDateOnly = () => {
|
||||||
|
const yesterday = new Date();
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
return formatLocalDateOnly(yesterday);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const startOfLocalDate = (value) => {
|
||||||
|
const date = parseLocalDateOnly(value);
|
||||||
|
if (!isValidDate(date)) {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
date.setHours(0, 0, 0, 0);
|
||||||
|
return date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const endOfLocalDate = (value) => {
|
||||||
|
const date = parseLocalDateOnly(value);
|
||||||
|
if (!isValidDate(date)) {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
date.setHours(23, 59, 59, 999);
|
||||||
|
return date;
|
||||||
|
};
|
||||||
+4
-3
@@ -19,6 +19,7 @@ import DepartmentDashboardDailyReportNavigation from "@/views/dashboards/departm
|
|||||||
import DepartmentDashboardDailyReportProductSales from "@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportProductSales.vue";
|
import DepartmentDashboardDailyReportProductSales from "@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportProductSales.vue";
|
||||||
import DepartmentDashboardDailyReportTodayForm from "@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportTodayForm.vue";
|
import DepartmentDashboardDailyReportTodayForm from "@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportTodayForm.vue";
|
||||||
import DepartmentWeather from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentWeather.vue";
|
import DepartmentWeather from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentWeather.vue";
|
||||||
|
import { parseLocalDateOnly, todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
import {
|
import {
|
||||||
complaints_metric_state,
|
complaints_metric_state,
|
||||||
count_bookings,
|
count_bookings,
|
||||||
@@ -131,7 +132,7 @@ onMounted(() => {
|
|||||||
return [...accessibleDepartmentIds.value];
|
return [...accessibleDepartmentIds.value];
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const initialDateFrom = typeof query.dateFrom === "string" ? query.dateFrom : new Date().toISOString().split("T")[0];
|
const initialDateFrom = typeof query.dateFrom === "string" ? query.dateFrom : todayLocalDateOnly();
|
||||||
const initialDateTo = typeof query.dateTo === "string" ? query.dateTo : initialDateFrom;
|
const initialDateTo = typeof query.dateTo === "string" ? query.dateTo : initialDateFrom;
|
||||||
|
|
||||||
initializeDailyReportFilters({
|
initializeDailyReportFilters({
|
||||||
@@ -170,7 +171,7 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const formatDateShort = (dateString) => {
|
const formatDateShort = (dateString) => {
|
||||||
const date = new Date(dateString);
|
const date = parseLocalDateOnly(dateString);
|
||||||
const dayName = date.toLocaleDateString("da-DK", { weekday: "long" });
|
const dayName = date.toLocaleDateString("da-DK", { weekday: "long" });
|
||||||
const dateValue = date.toLocaleDateString("da-DK", { day: "2-digit", month: "2-digit", year: "2-digit" });
|
const dateValue = date.toLocaleDateString("da-DK", { day: "2-digit", month: "2-digit", year: "2-digit" });
|
||||||
return `${dayName} d. ${dateValue}`;
|
return `${dayName} d. ${dateValue}`;
|
||||||
@@ -178,7 +179,7 @@ const formatDateShort = (dateString) => {
|
|||||||
|
|
||||||
const formatDateRangeSubtitle = (startDateString, endDateString) => {
|
const formatDateRangeSubtitle = (startDateString, endDateString) => {
|
||||||
const dateOptions = { day: "2-digit", month: "2-digit", year: "2-digit" };
|
const dateOptions = { day: "2-digit", month: "2-digit", year: "2-digit" };
|
||||||
return `${new Date(startDateString).toLocaleDateString("da-DK", dateOptions)} - ${new Date(endDateString).toLocaleDateString("da-DK", dateOptions)}`;
|
return `${parseLocalDateOnly(startDateString).toLocaleDateString("da-DK", dateOptions)} - ${parseLocalDateOnly(endDateString).toLocaleDateString("da-DK", dateOptions)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatSubtitle = (subtitle) => {
|
const formatSubtitle = (subtitle) => {
|
||||||
|
|||||||
+3
-5
@@ -2,18 +2,16 @@
|
|||||||
import { computed, ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import { createEmptyOutsideHours, normalizeOutsideHours } from "@/views/dashboards/departmentDashboard/modules/daily-report/outsideHours.js";
|
import { createEmptyOutsideHours, normalizeOutsideHours } from "@/views/dashboards/departmentDashboard/modules/daily-report/outsideHours.js";
|
||||||
|
import { todayLocalDateOnly, yesterdayLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
const GENERATE_UNIQUE_ID = () => Math.random().toString(36).slice(2, 11);
|
const GENERATE_UNIQUE_ID = () => Math.random().toString(36).slice(2, 11);
|
||||||
|
|
||||||
const DATE_TODAY = () => {
|
const DATE_TODAY = () => {
|
||||||
const date = new Date();
|
return todayLocalDateOnly();
|
||||||
return date.toISOString().split("T")[0];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const DATE_YESTERDAY = () => {
|
const DATE_YESTERDAY = () => {
|
||||||
const date = new Date();
|
return yesterdayLocalDateOnly();
|
||||||
date.setDate(date.getDate() - 1);
|
|
||||||
return date.toISOString().split("T")[0];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const date_shortcuts = ref({
|
const date_shortcuts = ref({
|
||||||
|
|||||||
+3
-9
@@ -17,6 +17,7 @@ import {
|
|||||||
isLoading as departmentsStoreLoading,
|
isLoading as departmentsStoreLoading,
|
||||||
} from "@/components/pagination/departmentTabs.vue";
|
} from "@/components/pagination/departmentTabs.vue";
|
||||||
import { isAccessibleVisibleDepartment, sortByDepartmentPriorityOrder } from "@/services/departmentVisibility.js";
|
import { isAccessibleVisibleDepartment, sortByDepartmentPriorityOrder } from "@/services/departmentVisibility.js";
|
||||||
|
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props:
|
* Props:
|
||||||
@@ -141,14 +142,7 @@ const toggleAllDepartments = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatDateSelectionValue = (date) => {
|
const formatDateSelectionValue = (date) => {
|
||||||
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
return formatLocalDateOnly(date);
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const year = date.getFullYear();
|
|
||||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
||||||
const day = String(date.getDate()).padStart(2, "0");
|
|
||||||
return `${year}-${month}-${day}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDateSelectionChange = (startDate, endDate) => {
|
const onDateSelectionChange = (startDate, endDate) => {
|
||||||
@@ -161,7 +155,7 @@ const onDateSelectionChange = (startDate, endDate) => {
|
|||||||
<div class="column is-12" data-testid="daily-report-date-controls">
|
<div class="column is-12" data-testid="daily-report-date-controls">
|
||||||
<DatePeriodSelector
|
<DatePeriodSelector
|
||||||
:on-selection-change="onDateSelectionChange"
|
:on-selection-change="onDateSelectionChange"
|
||||||
:selection="{ startDate: new Date(selected_date), endDate: new Date(selected_date_to) }"
|
:selection="{ startDate: parseLocalDateOnly(selected_date), endDate: parseLocalDateOnly(selected_date_to) }"
|
||||||
:visibility="{
|
:visibility="{
|
||||||
showDailySelector: true,
|
showDailySelector: true,
|
||||||
showWeeklySelector: true,
|
showWeeklySelector: true,
|
||||||
|
|||||||
+4
-3
@@ -5,6 +5,7 @@ import { parseError, getError, clearErrors} from "@/components/request/HandleGlo
|
|||||||
import ShowErrorField from "@/components/global/ShowErrorField.vue";
|
import ShowErrorField from "@/components/global/ShowErrorField.vue";
|
||||||
import { selected_date, onChangeCall, selected_department_id} from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue";
|
import { selected_date, onChangeCall, selected_department_id} from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
|
import { parseLocalDateOnly, yesterdayLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
department_id: Number,
|
department_id: Number,
|
||||||
@@ -23,7 +24,7 @@ const created_at = ref(null);
|
|||||||
const id = ref(null);
|
const id = ref(null);
|
||||||
const message = ref('');
|
const message = ref('');
|
||||||
const yesterday = ref({
|
const yesterday = ref({
|
||||||
date: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().split('T')[0],
|
date: yesterdayLocalDateOnly(),
|
||||||
water_usage: 0,
|
water_usage: 0,
|
||||||
water_usage_morning: 0,
|
water_usage_morning: 0,
|
||||||
notes: null,
|
notes: null,
|
||||||
@@ -104,7 +105,7 @@ const getDailyReport = async () => {
|
|||||||
// Check if the date is today
|
// Check if the date is today
|
||||||
const created_at_val = new Date(response.data.data.created_at);
|
const created_at_val = new Date(response.data.data.created_at);
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
if (created_at_val.toDateString() === (selected_date.value ? new Date(selected_date.value) : today).toDateString()) {
|
if (created_at_val.toDateString() === (selected_date.value ? parseLocalDateOnly(selected_date.value) : today).toDateString()) {
|
||||||
// The daily report is today
|
// The daily report is today
|
||||||
// So we can set the values
|
// So we can set the values
|
||||||
water_usage.value = parseInt(response.data.data.water_usage);
|
water_usage.value = parseInt(response.data.data.water_usage);
|
||||||
@@ -212,4 +213,4 @@ onChangeCall(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+4
-3
@@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { timeBookingsNewScheduler } from '@/views/dashboards/departmentDashboard/modules/time-bookings/book/TimeBookingsNewScheduler.vue';
|
import { timeBookingsNewScheduler } from '@/views/dashboards/departmentDashboard/modules/time-bookings/book/TimeBookingsNewScheduler.vue';
|
||||||
|
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
const date = ref();
|
const date = ref();
|
||||||
// Set the initial date to the current date YYYY-MM-DD format
|
// Set the initial date to the current date YYYY-MM-DD format
|
||||||
const formatDate = (date_input) => {
|
const formatDate = (date_input) => {
|
||||||
return date_input.toISOString().split('T')[0];
|
return formatLocalDateOnly(date_input);
|
||||||
};
|
};
|
||||||
date.value = formatDate(new Date());
|
date.value = formatDate(new Date());
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ watch(timeBookingsNewScheduler.date.selected, (newDate) => {
|
|||||||
|
|
||||||
watch(date, (newDate) => {
|
watch(date, (newDate) => {
|
||||||
// Update the selected date in the scheduler when the date input changes
|
// Update the selected date in the scheduler when the date input changes
|
||||||
timeBookingsNewScheduler.date.selected.value = new Date(newDate);
|
timeBookingsNewScheduler.date.selected.value = parseLocalDateOnly(newDate);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -63,4 +64,4 @@ watch(date, (newDate) => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { SessionUser } from '@/components/session/token/SessionUser.vue';
|
import { SessionUser } from '@/components/session/token/SessionUser.vue';
|
||||||
|
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
department_id: {
|
department_id: {
|
||||||
@@ -68,8 +69,7 @@ const getBookings = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getDate = () => {
|
const getDate = () => {
|
||||||
const date = new Date();
|
return todayLocalDateOnly();
|
||||||
return date.toISOString().split('T')[0];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the department
|
// Get the department
|
||||||
|
|||||||
+4
-3
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { selected_date, selected_date_to, selectDate as selectDailyReportDate } from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue";
|
import { selected_date, selected_date_to, selectDate as selectDailyReportDate } from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue";
|
||||||
import DatePeriodSelector from "@/components/displays/buttons/DatePeriodSelector.vue";
|
import DatePeriodSelector from "@/components/displays/buttons/DatePeriodSelector.vue";
|
||||||
|
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
// Emits for parents that want to react to date changes (non-breaking if unused)
|
// Emits for parents that want to react to date changes (non-breaking if unused)
|
||||||
const emit = defineEmits(['date-change']);
|
const emit = defineEmits(['date-change']);
|
||||||
@@ -16,12 +17,12 @@ const selectDate = (startIso, endIso) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DatePeriodSelector
|
<DatePeriodSelector
|
||||||
:on-selection-change="(startDate, endDate) => selectDate(startDate.toISOString().split('T')[0], endDate.toISOString().split('T')[0])"
|
:on-selection-change="(startDate, endDate) => selectDate(formatLocalDateOnly(startDate), formatLocalDateOnly(endDate))"
|
||||||
:selection="{ startDate: new Date(selected_date), endDate: new Date(selected_date_to) }"
|
:selection="{ startDate: parseLocalDateOnly(selected_date), endDate: parseLocalDateOnly(selected_date_to) }"
|
||||||
:visibility="{ showDailySelector: true, showWeeklySelector: true, showMultipleMonthWarning: false, showUpdateButton: false }"
|
:visibility="{ showDailySelector: true, showWeeklySelector: true, showMultipleMonthWarning: false, showUpdateButton: false }"
|
||||||
:reverse-level-order="true"
|
:reverse-level-order="true"
|
||||||
@update:selection="(newSelection) => {
|
@update:selection="(newSelection) => {
|
||||||
selectDate(newSelection.startDate.toISOString().split('T')[0], newSelection.endDate.toISOString().split('T')[0]);
|
selectDate(formatLocalDateOnly(newSelection.startDate), formatLocalDateOnly(newSelection.endDate));
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+4
-3
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {defineComponent, ref, watch} from 'vue';
|
import {defineComponent, ref, watch} from 'vue';
|
||||||
|
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
|
|
||||||
type fetch = {
|
type fetch = {
|
||||||
@@ -11,8 +12,8 @@ type fetch = {
|
|||||||
};
|
};
|
||||||
/** Define the variables */
|
/** Define the variables */
|
||||||
export const show_this_week = ref(false);
|
export const show_this_week = ref(false);
|
||||||
export const selected_date = ref(new Date().toISOString().split('T')[0]);
|
export const selected_date = ref(todayLocalDateOnly());
|
||||||
export const selected_date_to = ref(new Date().toISOString().split('T')[0]);
|
export const selected_date_to = ref(todayLocalDateOnly());
|
||||||
export const last_fetch_id = ref(0);
|
export const last_fetch_id = ref(0);
|
||||||
export const fetches = ref<fetch[]>([]);
|
export const fetches = ref<fetch[]>([]);
|
||||||
/** Define the functions */
|
/** Define the functions */
|
||||||
@@ -53,4 +54,4 @@ watch([selected_date, selected_date_to], () => {
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'DepartmentsOverviewObject'
|
name: 'DepartmentsOverviewObject'
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,21 +1,53 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { getOrders } from "@/components/shop/Orders.vue";
|
import { ref } from "vue";
|
||||||
import { showCreateOrderForm } from "@/components/forms/superUser/createOrderForm.vue";
|
|
||||||
import SuperUserDashboardNavigation from "@/views/dashboards/superUserDashboard/SuperUserDashboardNavigation.vue";
|
import SuperUserDashboardNavigation from "@/views/dashboards/superUserDashboard/SuperUserDashboardNavigation.vue";
|
||||||
import PageTitle from "@/components/global/PageTitle.vue";
|
|
||||||
import Orders from "@/components/displays/Orders.vue";
|
|
||||||
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
|
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import VehiclesPagination from "@/components/displays/pagination/models/UserDashboard/VehiclesPagination.vue";
|
import VehiclesPagination from "@/components/displays/pagination/models/UserDashboard/VehiclesPagination.vue";
|
||||||
|
import SuperuserAddVehicleModal from "@/components/forms/superUser/SuperuserAddVehicleModal.vue";
|
||||||
|
import { loadList } from "@/components/pagination/paginatedList.vue";
|
||||||
|
|
||||||
|
const isAddVehicleModalOpen = ref(false);
|
||||||
|
|
||||||
|
const openAddVehicleModal = () => {
|
||||||
|
isAddVehicleModalOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeAddVehicleModal = () => {
|
||||||
|
isAddVehicleModalOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVehicleCreated = async () => {
|
||||||
|
closeAddVehicleModal();
|
||||||
|
await loadList();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessSuperUser()">
|
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessSuperUser()">
|
||||||
<SuperUserDashboardNavigation />
|
<SuperUserDashboardNavigation />
|
||||||
<VehiclesPagination />
|
<VehiclesPagination>
|
||||||
|
<template #actions>
|
||||||
|
<button
|
||||||
|
class="button is-link button-same-width superuser-vehicles__add-button"
|
||||||
|
type="button"
|
||||||
|
data-testid="superuser-vehicles-add"
|
||||||
|
@click="openAddVehicleModal"
|
||||||
|
>
|
||||||
|
{{ $t("vehicles.add") }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</VehiclesPagination>
|
||||||
|
<SuperuserAddVehicleModal
|
||||||
|
v-if="isAddVehicleModalOpen"
|
||||||
|
@close="closeAddVehicleModal"
|
||||||
|
@created="handleVehicleCreated"
|
||||||
|
/>
|
||||||
</RestrictedPageWrapper>
|
</RestrictedPageWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.superuser-vehicles__add-button {
|
||||||
</style>
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import StatisticsDepartmentGoal
|
|||||||
from "@/views/dashboards/superUserDashboard/statistics/displays/overview/StatisticsDepartmentGoal.vue";
|
from "@/views/dashboards/superUserDashboard/statistics/displays/overview/StatisticsDepartmentGoal.vue";
|
||||||
import {departments, getDepartments} from "@/components/pagination/departmentTabs.vue";
|
import {departments, getDepartments} from "@/components/pagination/departmentTabs.vue";
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -70,7 +71,8 @@ getDepartments()
|
|||||||
// Get the statistics
|
// Get the statistics
|
||||||
const getStatistics = async () => {
|
const getStatistics = async () => {
|
||||||
// Get the statistics
|
// Get the statistics
|
||||||
const response = await authenticatedRequest('/statistics/income/departments?start_date=' + (new Date()).toISOString().split('T')[0] + '&end_date=' + (new Date()).toISOString().split('T')[0])
|
const today = todayLocalDateOnly();
|
||||||
|
const response = await authenticatedRequest('/statistics/income/departments?start_date=' + today + '&end_date=' + today)
|
||||||
// Get the sent data
|
// Get the sent data
|
||||||
statistics_department_today.value = response.data.data
|
statistics_department_today.value = response.data.data
|
||||||
// Log the data
|
// Log the data
|
||||||
@@ -131,4 +133,4 @@ getStatistics()
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {ref} from 'vue';
|
|||||||
import { Bar } from 'vue-chartjs';
|
import { Bar } from 'vue-chartjs';
|
||||||
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale } from 'chart.js'
|
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale } from 'chart.js'
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
|
|
||||||
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
|
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
|
||||||
@@ -16,7 +17,7 @@ const chartOptions = {
|
|||||||
// Shortcut to statistics
|
// Shortcut to statistics
|
||||||
|
|
||||||
// Set the time for the statistics to today
|
// Set the time for the statistics to today
|
||||||
SessionUser.adminUser.statistics.set_time((new Date()).toISOString().split('T')[0], (new Date()).toISOString().split('T')[0])
|
SessionUser.adminUser.statistics.set_time(todayLocalDateOnly(), todayLocalDateOnly())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -33,4 +34,4 @@ SessionUser.adminUser.statistics.set_time((new Date()).toISOString().split('T')[
|
|||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,16 +15,20 @@ function isApiUrl(url: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mockHtml2Canvas(page) {
|
const html2CanvasModulePattern = /\/(?:node_modules\/.*)?html2canvas(?:\.[\w-]+)?\.js(?:\?.*)?$/i;
|
||||||
await page.route(/\/node_modules\/html2canvas\/dist\/html2canvas\.esm\.js(?:\?.*)?$/i, async (route) => {
|
|
||||||
|
async function mockHtml2Canvas(page, { alwaysFail = false } = {}) {
|
||||||
|
await page.unroute(html2CanvasModulePattern).catch(() => {});
|
||||||
|
await page.route(html2CanvasModulePattern, async (route) => {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
contentType: "text/javascript",
|
contentType: "text/javascript",
|
||||||
body: `
|
body: `
|
||||||
|
const alwaysFail = ${alwaysFail ? "true" : "false"};
|
||||||
let html2canvasAttempts = 0;
|
let html2canvasAttempts = 0;
|
||||||
export default async function html2canvas() {
|
export default async function html2canvas() {
|
||||||
html2canvasAttempts += 1;
|
html2canvasAttempts += 1;
|
||||||
if (html2canvasAttempts === 1) {
|
if (alwaysFail || html2canvasAttempts === 1) {
|
||||||
throw new Error("Simulated first capture failure");
|
throw new Error(alwaysFail ? "Simulated permanent capture failure" : "Simulated first capture failure");
|
||||||
}
|
}
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = 8;
|
canvas.width = 8;
|
||||||
@@ -163,10 +167,68 @@ test.describe("Authenticated error reports", () => {
|
|||||||
expect(Array.isArray(body.vue_errors)).toBe(true);
|
expect(Array.isArray(body.vue_errors)).toBe(true);
|
||||||
expect(body.context).toMatchObject({
|
expect(body.context).toMatchObject({
|
||||||
data_collection_policy_version: "error-report-v1",
|
data_collection_policy_version: "error-report-v1",
|
||||||
|
screenshot_attachment: {
|
||||||
|
status: "stored",
|
||||||
|
attached: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await expect(page.getByTestId("error-report-submitted")).toBeVisible();
|
await expect(page.getByTestId("error-report-submitted")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("submits without a screenshot when capture never succeeds", async ({ page }) => {
|
||||||
|
await mockHtml2Canvas(page, { alwaysFail: true });
|
||||||
|
const submittedBodies: Array<Record<string, unknown>> = [];
|
||||||
|
|
||||||
|
await page.route(/\/error-reports(?:\?.*)?$/i, async (route) => {
|
||||||
|
if (!isApiUrl(route.request().url())) {
|
||||||
|
await route.fallback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (route.request().method().toUpperCase() !== "POST") {
|
||||||
|
await route.fallback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const body = route.request().postDataJSON() as Record<string, unknown>;
|
||||||
|
submittedBodies.push(body);
|
||||||
|
await route.fulfill(
|
||||||
|
json(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
id: 100,
|
||||||
|
status: "open",
|
||||||
|
screenshot: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
201
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await primeMockSession(page, { token: "error-report-user-token", bootPath: "/admin" });
|
||||||
|
await openErrorReportLauncher(page);
|
||||||
|
|
||||||
|
await page.getByLabel("What were you doing before the error occurred?").fill("Opening the orders page");
|
||||||
|
await page.getByLabel("What did you expect would happen?").fill("The orders should load");
|
||||||
|
await page.getByLabel("What actually happened?").fill("The page showed an error");
|
||||||
|
await page.getByTestId("error-report-consent").check();
|
||||||
|
await page.getByTestId("error-report-submit").click();
|
||||||
|
|
||||||
|
await expect.poll(() => submittedBodies.length).toBe(1);
|
||||||
|
const body = submittedBodies[0];
|
||||||
|
expect(body.screenshot).toBeNull();
|
||||||
|
expect(body.context).toMatchObject({
|
||||||
|
screenshot_attachment: {
|
||||||
|
status: "capture_failed",
|
||||||
|
attached: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await expect(page.getByTestId("error-report-capture-warning")).toContainText(
|
||||||
|
"The screen capture failed. The report will be sent without a screenshot."
|
||||||
|
);
|
||||||
|
await expect(page.getByTestId("error-report-submitted")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test("lets superusers inspect and resolve submitted reports", async ({ page }) => {
|
test("lets superusers inspect and resolve submitted reports", async ({ page }) => {
|
||||||
const report: Record<string, any> = {
|
const report: Record<string, any> = {
|
||||||
id: 12,
|
id: 12,
|
||||||
@@ -277,4 +339,90 @@ test.describe("Authenticated error reports", () => {
|
|||||||
]);
|
]);
|
||||||
await expect(page.getByTestId("error-report-detail")).toContainText("resolved");
|
await expect(page.getByTestId("error-report-detail")).toContainText("resolved");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("lets superusers inspect submitted reports without screenshots", async ({ page }) => {
|
||||||
|
const report: Record<string, any> = {
|
||||||
|
id: 13,
|
||||||
|
status: "open",
|
||||||
|
reporter: {
|
||||||
|
type: "user",
|
||||||
|
user_id: 7,
|
||||||
|
customer_number: 12345,
|
||||||
|
name: "Error Reporter",
|
||||||
|
email: "reporter@example.test",
|
||||||
|
},
|
||||||
|
route_path: "/user/orders",
|
||||||
|
page_url: "https://app.example.test/user/orders",
|
||||||
|
release_trace_id: "trace-no-screenshot",
|
||||||
|
frontend_version: "front-1",
|
||||||
|
api_version: "api-1",
|
||||||
|
screenshot: null,
|
||||||
|
answers: {
|
||||||
|
before_error: "Opening the orders page",
|
||||||
|
expected: "Orders should load",
|
||||||
|
actual: "The table stayed empty",
|
||||||
|
},
|
||||||
|
request_error_count: 0,
|
||||||
|
vue_error_count: 0,
|
||||||
|
request_errors: [],
|
||||||
|
vue_errors: [],
|
||||||
|
runtime_context: {
|
||||||
|
screenshot_attachment: {
|
||||||
|
status: "capture_failed",
|
||||||
|
attached: false,
|
||||||
|
mime_type: null,
|
||||||
|
size_bytes: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created_at: "2026-05-19 08:00:00",
|
||||||
|
updated_at: "2026-05-19 08:00:00",
|
||||||
|
resolved_at: null,
|
||||||
|
resolved_by_user_id: null,
|
||||||
|
resolution_note: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await page.route(/\/superuser\/error-reports(?:\?.*)?$/i, async (route) => {
|
||||||
|
if (!isApiUrl(route.request().url())) {
|
||||||
|
await route.fallback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (route.request().method().toUpperCase() !== "GET") {
|
||||||
|
await route.fallback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await route.fulfill(
|
||||||
|
json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
items: [report],
|
||||||
|
counts: {
|
||||||
|
open: 1,
|
||||||
|
resolved: 0,
|
||||||
|
all: 1,
|
||||||
|
},
|
||||||
|
limit: 100,
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route(/\/superuser\/error-reports\/13$/i, async (route) => {
|
||||||
|
if (!isApiUrl(route.request().url())) {
|
||||||
|
await route.fallback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await route.fulfill(json({ success: true, data: report }));
|
||||||
|
});
|
||||||
|
|
||||||
|
await primeMockSession(page, { token: "error-report-superuser-token", bootPath: "/superuser/error-reports" });
|
||||||
|
await expect(page.getByTestId("error-reports-page")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("error-report-list")).toContainText("/user/orders");
|
||||||
|
|
||||||
|
await page.getByTestId("error-report-view").click();
|
||||||
|
const detail = page.getByTestId("error-report-detail");
|
||||||
|
await expect(detail).toContainText("Opening the orders page");
|
||||||
|
await expect(detail).toContainText("trace-no-screenshot");
|
||||||
|
await expect(detail.locator("img")).toHaveCount(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3272,6 +3272,66 @@ test.describe("POS mobile order flow", () => {
|
|||||||
expect(createdProductIds).toEqual([53, 71, 91]);
|
expect(createdProductIds).toEqual([53, 71, 91]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("manual step 2 blocks additional items when customer restricts additional services", async ({ page }) => {
|
||||||
|
const orderId = 9414;
|
||||||
|
const fixture = createMobilePosFixture({
|
||||||
|
customerAttributesByNumber: {
|
||||||
|
[REGULAR_CUSTOMER_ID]: [
|
||||||
|
{
|
||||||
|
id: 941401,
|
||||||
|
customer_number: REGULAR_CUSTOMER_ID,
|
||||||
|
attribute: "restrictAdditionalServices",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
ordersById: {
|
||||||
|
[orderId]: buildRegularOrder(orderId, {
|
||||||
|
reference: "STEP2-RESTRICT-ADDITIONAL",
|
||||||
|
reg_1: "ZZ00000",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
orderItemsByOrderId: {
|
||||||
|
[orderId]: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await setupMobilePosPage(page, fixture, {
|
||||||
|
token: "mobile-step2-restrict-additional-token",
|
||||||
|
seedState: {
|
||||||
|
customerId: REGULAR_CUSTOMER_ID,
|
||||||
|
reg: "ZZ00000",
|
||||||
|
reference: "STEP2-RESTRICT-ADDITIONAL",
|
||||||
|
includePrimaryItem: false,
|
||||||
|
vehicleType: null,
|
||||||
|
lastOrderId: null,
|
||||||
|
},
|
||||||
|
route: {
|
||||||
|
step: 2,
|
||||||
|
orderId,
|
||||||
|
customerId: REGULAR_CUSTOMER_ID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect.poll(() => fixture.requestCounters.customerAttributesGet, { timeout: 10_000 }).toBeGreaterThan(0);
|
||||||
|
await selectPrimaryProduct(page, 53);
|
||||||
|
await expect(page.getByTestId("pos-mobile-additional-items-open")).toHaveCount(1);
|
||||||
|
|
||||||
|
await page.getByTestId("pos-mobile-additional-items-open").click();
|
||||||
|
await expect(page.getByTestId("pos-mobile-additional-items-selection")).toHaveCount(0);
|
||||||
|
|
||||||
|
await longPressAdditionalItems(page);
|
||||||
|
await expect(page.getByTestId("pos-mobile-additional-items-selection")).toHaveCount(0);
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const snapshot = await getStoredPosSnapshot(page);
|
||||||
|
return snapshot?.transactionItems?.additionalItems || [];
|
||||||
|
},
|
||||||
|
{ timeout: 10_000 }
|
||||||
|
)
|
||||||
|
.toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
test("clear all deletes the order and returns to the scanner", async ({ page }) => {
|
test("clear all deletes the order and returns to the scanner", async ({ page }) => {
|
||||||
const orderId = 9403;
|
const orderId = 9403;
|
||||||
const fixture = createMobilePosFixture({
|
const fixture = createMobilePosFixture({
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ async function primeSuperuserSession(page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test.describe("Superuser vehicles smoke", () => {
|
test.describe("Superuser vehicles smoke", () => {
|
||||||
|
test.describe.configure({ mode: "serial" });
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await mockApi(page, {
|
await mockApi(page, {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
permissions: ["superuser", "user"],
|
permissions: ["superuser", "user"],
|
||||||
|
pos: true,
|
||||||
});
|
});
|
||||||
await primeSuperuserSession(page);
|
await primeSuperuserSession(page);
|
||||||
});
|
});
|
||||||
@@ -23,4 +26,51 @@ test.describe("Superuser vehicles smoke", () => {
|
|||||||
await expect(page.locator("body")).toContainText(/registrerede|registered/i);
|
await expect(page.locator("body")).toContainText(/registrerede|registered/i);
|
||||||
await expect(page.locator("body")).not.toContainText(/Order ID is required/i);
|
await expect(page.locator("body")).not.toContainText(/Order ID is required/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("superuser can add a vehicle after selecting a customer from searchable results", async ({ page }) => {
|
||||||
|
const createVehiclePayloads = [];
|
||||||
|
|
||||||
|
page.on("request", (request) => {
|
||||||
|
const url = new URL(request.url());
|
||||||
|
if (url.pathname.endsWith("/vehicles") && request.method() === "POST") {
|
||||||
|
createVehiclePayloads.push(request.postDataJSON());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto("/superuser/vehicles");
|
||||||
|
|
||||||
|
await page.getByTestId("superuser-vehicles-add").click();
|
||||||
|
await expect(page.getByTestId("superuser-add-vehicle-modal")).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTestId("superuser-add-vehicle-customer-input").fill("12345679");
|
||||||
|
await expect(page.getByTestId("superuser-add-vehicle-customer-option-0")).toBeVisible();
|
||||||
|
await page.getByTestId("superuser-add-vehicle-customer-option-0").click();
|
||||||
|
await expect(page.getByTestId("superuser-add-vehicle-customer-selected")).toContainText("#12345679");
|
||||||
|
|
||||||
|
await page.getByTestId("superuser-add-vehicle-registration").fill("ab12345");
|
||||||
|
await expect(page.getByTestId("superuser-add-vehicle-type")).toBeEnabled();
|
||||||
|
await page.getByTestId("superuser-add-vehicle-type").selectOption("53");
|
||||||
|
await page.getByTestId("superuser-add-vehicle-wash-subscription").check();
|
||||||
|
await page.getByTestId("superuser-add-vehicle-reference").fill("Fleet reference");
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForResponse(
|
||||||
|
(response) => response.url().includes("/vehicles") && response.request().method() === "POST"
|
||||||
|
),
|
||||||
|
page.getByTestId("superuser-add-vehicle-submit").click(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(createVehiclePayloads).toEqual([
|
||||||
|
{
|
||||||
|
type: 53,
|
||||||
|
reg: "AB12345",
|
||||||
|
wash_subscription: true,
|
||||||
|
customer_id: 12345679,
|
||||||
|
reference: "Fleet reference",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(page.getByTestId("superuser-add-vehicle-modal")).toBeHidden();
|
||||||
|
await expect(page.getByTestId("user-vehicles-table")).toContainText("AB12345");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2736,6 +2736,7 @@ export function createPosFixture(overrides = {}) {
|
|||||||
last_order_id: 54518,
|
last_order_id: 54518,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
nextVehicleId: 7002,
|
||||||
unknownVehicles: [],
|
unknownVehicles: [],
|
||||||
orderBookings: [],
|
orderBookings: [],
|
||||||
bookingOrderAssignments: [],
|
bookingOrderAssignments: [],
|
||||||
@@ -3278,6 +3279,30 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pathname.endsWith("/vehicles") && method === "POST") {
|
||||||
|
const body = request.postDataJSON?.() || {};
|
||||||
|
const customerId = Number(body.customer_id || 0);
|
||||||
|
const customer = posFixture.customersByNumber[customerId] || null;
|
||||||
|
const vehicle = {
|
||||||
|
id: posFixture.nextVehicleId || 9001,
|
||||||
|
reg: String(body.reg || "").toUpperCase(),
|
||||||
|
customer_id: customerId,
|
||||||
|
customer_name: customer?.name || "",
|
||||||
|
type: Number(body.type || 0),
|
||||||
|
status: "verified",
|
||||||
|
barred: false,
|
||||||
|
wash_subscription: Boolean(body.wash_subscription),
|
||||||
|
addons: { enabled: 0, available: 0, list: [] },
|
||||||
|
reference: body.reference || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
posFixture.nextVehicleId = vehicle.id + 1;
|
||||||
|
posFixture.vehicles = [vehicle, ...(posFixture.vehicles || [])];
|
||||||
|
|
||||||
|
await route.fulfill(json({ success: true, data: vehicle }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (pathname.endsWith("/department/vehicles/unknown-customer") && method === "GET") {
|
if (pathname.endsWith("/department/vehicles/unknown-customer") && method === "GET") {
|
||||||
await route.fulfill(json({ success: true, data: posFixture.unknownVehicles || [] }));
|
await route.fulfill(json({ success: true, data: posFixture.unknownVehicles || [] }));
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import { nextTick } from "vue";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { mountWithApp } from "./helpers/mountWithApp.js";
|
||||||
|
|
||||||
|
const searchMocks = vi.hoisted(() => ({
|
||||||
|
isSearchingRef: null,
|
||||||
|
resultsRef: null,
|
||||||
|
searchCustomer: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/search/economic/customerSearch.vue", async () => {
|
||||||
|
const { ref } = await vi.importActual("vue");
|
||||||
|
|
||||||
|
searchMocks.isSearchingRef = ref(false);
|
||||||
|
searchMocks.resultsRef = ref([]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSearching: searchMocks.isSearchingRef,
|
||||||
|
searchCustomerResults: searchMocks.resultsRef,
|
||||||
|
searchCustomer: searchMocks.searchCustomer,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import CustomerSearchSelect from "@/components/search/economic/CustomerSearchSelect.vue";
|
||||||
|
|
||||||
|
const customers = [
|
||||||
|
{
|
||||||
|
customerNumber: 12345679,
|
||||||
|
name: "Acme Transport",
|
||||||
|
city: "Taastrup",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerNumber: 87654321,
|
||||||
|
name: "Nordic Wash",
|
||||||
|
city: "Copenhagen",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
en: {
|
||||||
|
vehicles: {
|
||||||
|
add_modal: {
|
||||||
|
customer_label: "Customer",
|
||||||
|
customer_placeholder: "Search customers",
|
||||||
|
selected_customer: "Selected customer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
clear: "Clear",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mountComponent = (props = {}) =>
|
||||||
|
mountWithApp(CustomerSearchSelect, {
|
||||||
|
props: {
|
||||||
|
inputId: "test-customer-search",
|
||||||
|
testIdPrefix: "test-customer",
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
messages,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("CustomerSearchSelect", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
searchMocks.isSearchingRef.value = false;
|
||||||
|
searchMocks.resultsRef.value = [];
|
||||||
|
searchMocks.searchCustomer.mockImplementation((query) => {
|
||||||
|
searchMocks.resultsRef.value = query ? customers : [];
|
||||||
|
return Promise.resolve(searchMocks.resultsRef.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("searches customers and emits the selected customer", async () => {
|
||||||
|
const wrapper = mountComponent();
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="test-customer-input"]').setValue("acme");
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(searchMocks.searchCustomer).toHaveBeenLastCalledWith("acme");
|
||||||
|
expect(wrapper.get('[data-testid="test-customer-option-0"]').text()).toContain("Acme Transport");
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="test-customer-option-0"]').trigger("mousedown");
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.emitted("update:modelValue").at(-1)).toEqual([customers[0]]);
|
||||||
|
expect(wrapper.emitted("selected").at(-1)).toEqual([customers[0]]);
|
||||||
|
await wrapper.setProps({ modelValue: customers[0] });
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.get('[data-testid="test-customer-selected"]').text()).toContain("Acme Transport");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports keyboard selection and clearing", async () => {
|
||||||
|
const wrapper = mountComponent();
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="test-customer-input"]').setValue("nordic");
|
||||||
|
await wrapper.get('[data-testid="test-customer-input"]').trigger("keydown", { key: "ArrowDown" });
|
||||||
|
await wrapper.get('[data-testid="test-customer-input"]').trigger("keydown", { key: "ArrowDown" });
|
||||||
|
await wrapper.get('[data-testid="test-customer-input"]').trigger("keydown", { key: "Enter" });
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.emitted("update:modelValue").at(-1)).toEqual([customers[1]]);
|
||||||
|
await wrapper.setProps({ modelValue: customers[1] });
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="test-customer-clear"]').trigger("click");
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.emitted("update:modelValue").at(-1)).toEqual([null]);
|
||||||
|
expect(wrapper.emitted("cleared")).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { endOfLocalDate, formatLocalDateOnly, parseLocalDateOnly, startOfLocalDate } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
|
describe("date-only helpers", () => {
|
||||||
|
it("formats Date objects from their local calendar date", () => {
|
||||||
|
expect(formatLocalDateOnly(new Date(2026, 2, 31, 0, 0, 0, 0))).toBe("2026-03-31");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses date input values as local calendar dates", () => {
|
||||||
|
const parsed = parseLocalDateOnly("2026-03-31");
|
||||||
|
|
||||||
|
expect(parsed.getFullYear()).toBe(2026);
|
||||||
|
expect(parsed.getMonth()).toBe(2);
|
||||||
|
expect(parsed.getDate()).toBe(31);
|
||||||
|
expect(formatLocalDateOnly(parsed)).toBe("2026-03-31");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps date-only prefixes from API timestamps without timezone reinterpretation", () => {
|
||||||
|
expect(formatLocalDateOnly("2026-03-31T23:30:00.000Z")).toBe("2026-03-31");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects impossible date input values", () => {
|
||||||
|
expect(Number.isNaN(parseLocalDateOnly("2026-02-31").getTime())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds local day boundaries from the selected date", () => {
|
||||||
|
expect(startOfLocalDate("2026-03-31").getHours()).toBe(0);
|
||||||
|
expect(startOfLocalDate("2026-03-31").getMinutes()).toBe(0);
|
||||||
|
expect(endOfLocalDate("2026-03-31").getHours()).toBe(23);
|
||||||
|
expect(endOfLocalDate("2026-03-31").getMinutes()).toBe(59);
|
||||||
|
expect(formatLocalDateOnly(endOfLocalDate("2026-03-31"))).toBe("2026-03-31");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -122,6 +122,32 @@ describe("DatePeriodSelector mobile layout", () => {
|
|||||||
expect(formatLocalDate(onSelectionChange.mock.calls[0][0])).toBe("2026-03-01");
|
expect(formatLocalDate(onSelectionChange.mock.calls[0][0])).toBe("2026-03-01");
|
||||||
expect(formatLocalDate(onSelectionChange.mock.calls[0][1])).toBe("2026-03-05");
|
expect(formatLocalDate(onSelectionChange.mock.calls[0][1])).toBe("2026-03-05");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("emits the exact clicked date for start and end inputs", async () => {
|
||||||
|
sharedState.width.value = 1024;
|
||||||
|
const onSelectionChange = vi.fn();
|
||||||
|
const wrapper = mount(DatePeriodSelector, {
|
||||||
|
props: {
|
||||||
|
selection: {
|
||||||
|
startDate: new Date(2026, 2, 1, 0, 0, 0, 0),
|
||||||
|
endDate: new Date(2026, 2, 3, 23, 59, 59, 999),
|
||||||
|
},
|
||||||
|
onSelectionChange,
|
||||||
|
visibility: {
|
||||||
|
showUpdateButton: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await wrapper.get("[data-testid='date-period-start']").setValue("2026-03-31");
|
||||||
|
await wrapper.get("[data-testid='date-period-end']").setValue("2026-04-02");
|
||||||
|
|
||||||
|
const emittedSelection = wrapper.emitted("update:selection")?.at(-1)?.[0];
|
||||||
|
expect(formatLocalDate(emittedSelection.startDate)).toBe("2026-03-31");
|
||||||
|
expect(formatLocalDate(emittedSelection.endDate)).toBe("2026-04-02");
|
||||||
|
expect(formatLocalDate(onSelectionChange.mock.calls.at(-1)[0])).toBe("2026-03-31");
|
||||||
|
expect(formatLocalDate(onSelectionChange.mock.calls.at(-1)[1])).toBe("2026-04-02");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("DatePeriodSelector month warning", () => {
|
describe("DatePeriodSelector month warning", () => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { defineComponent, nextTick } from "vue";
|
import { defineComponent, nextTick } from "vue";
|
||||||
import { mount } from "@vue/test-utils";
|
import { mount } from "@vue/test-utils";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { formatLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
const selectDateMock = vi.hoisted(() => vi.fn());
|
const selectDateMock = vi.hoisted(() => vi.fn());
|
||||||
const sharedDateState = vi.hoisted(() => ({
|
const sharedDateState = vi.hoisted(() => ({
|
||||||
@@ -39,10 +40,10 @@ const DatePeriodSelectorStub = defineComponent({
|
|||||||
emits: ["update:selection"],
|
emits: ["update:selection"],
|
||||||
template: `
|
template: `
|
||||||
<div>
|
<div>
|
||||||
<button class="trigger-on-selection-change" @click="onSelectionChange(new Date('2026-03-24T00:00:00.000Z'), new Date('2026-03-29T00:00:00.000Z'))">
|
<button class="trigger-on-selection-change" @click="onSelectionChange(new Date(2026, 2, 24), new Date(2026, 2, 29))">
|
||||||
onSelectionChange
|
onSelectionChange
|
||||||
</button>
|
</button>
|
||||||
<button class="trigger-update-selection" @click="$emit('update:selection', { startDate: new Date('2026-04-01T00:00:00.000Z'), endDate: new Date('2026-04-30T00:00:00.000Z') })">
|
<button class="trigger-update-selection" @click="$emit('update:selection', { startDate: new Date(2026, 3, 1), endDate: new Date(2026, 3, 30) })">
|
||||||
update:selection
|
update:selection
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,7 +106,7 @@ describe("DepartmentDashboardOverviewNavigation", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const selector = wrapper.findComponent(DatePeriodSelectorStub);
|
const selector = wrapper.findComponent(DatePeriodSelectorStub);
|
||||||
expect(selector.props("selection").startDate.toISOString().split("T")[0]).toBe("2026-05-05");
|
expect(formatLocalDateOnly(selector.props("selection").startDate)).toBe("2026-05-05");
|
||||||
expect(selector.props("selection").endDate.toISOString().split("T")[0]).toBe("2026-05-11");
|
expect(formatLocalDateOnly(selector.props("selection").endDate)).toBe("2026-05-11");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mount } from "@vue/test-utils";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("vue-i18n", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useI18n: () => ({
|
||||||
|
t: (value) => value,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
||||||
|
SessionUser: {
|
||||||
|
objects: {
|
||||||
|
global: {
|
||||||
|
language: {
|
||||||
|
date_from: "From",
|
||||||
|
date_to: "To",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { formatLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
import PaginationDisplayTemplateDate from "@/components/displays/pagination/templates/PaginationDisplayTemplateDate.vue";
|
||||||
|
import PaginationDisplayTemplateDates from "@/components/displays/pagination/templates/PaginationDisplayTemplateDates.vue";
|
||||||
|
import { dateFunctions } from "@/components/displays/pagination/PaginationDisplayDates.vue";
|
||||||
|
|
||||||
|
const PaginationDisplayItemColumnStub = defineComponent({
|
||||||
|
name: "PaginationDisplayItemColumn",
|
||||||
|
props: {
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: "<div><slot name='control' /></div>",
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("pagination date selection", () => {
|
||||||
|
it("renders Date model values as the same local date", () => {
|
||||||
|
const wrapper = mount(PaginationDisplayTemplateDate, {
|
||||||
|
props: {
|
||||||
|
label: "Date",
|
||||||
|
modelValue: new Date(2026, 2, 31, 0, 0, 0, 0),
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
PaginationDisplayItemColumn: PaginationDisplayItemColumnStub,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.get("input[type='date']").element.value).toBe("2026-03-31");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits the clicked input date as the same local date", async () => {
|
||||||
|
const wrapper = mount(PaginationDisplayTemplateDate, {
|
||||||
|
props: {
|
||||||
|
label: "Date",
|
||||||
|
modelValue: new Date(2026, 2, 1, 0, 0, 0, 0),
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
PaginationDisplayItemColumn: PaginationDisplayItemColumnStub,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await wrapper.get("input[type='date']").setValue("2026-03-31");
|
||||||
|
|
||||||
|
const emittedDate = wrapper.emitted("update:date")?.at(-1)?.[0];
|
||||||
|
expect(formatLocalDateOnly(emittedDate)).toBe("2026-03-31");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the real first day of the month for month presets", () => {
|
||||||
|
const firstDay = dateFunctions.datePresetFunctions.month.firstDayOfMonth(new Date(2026, 4, 15, 12, 0, 0, 0));
|
||||||
|
|
||||||
|
expect(formatLocalDateOnly(firstDay)).toBe("2026-05-01");
|
||||||
|
expect(firstDay.getHours()).toBe(0);
|
||||||
|
expect(firstDay.getMinutes()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits both start and end updates from the paired date selector", async () => {
|
||||||
|
const wrapper = mount(PaginationDisplayTemplateDates, {
|
||||||
|
props: {
|
||||||
|
startDate: new Date(2026, 2, 1, 0, 0, 0, 0),
|
||||||
|
endDate: new Date(2026, 2, 31, 0, 0, 0, 0),
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
PaginationDisplayItemColumn: PaginationDisplayItemColumnStub,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputs = wrapper.findAll("input[type='date']");
|
||||||
|
await inputs[0].setValue("2026-04-01");
|
||||||
|
await inputs[1].setValue("2026-04-30");
|
||||||
|
|
||||||
|
expect(formatLocalDateOnly(wrapper.emitted("update:startDate")?.at(-1)?.[0])).toBe("2026-04-01");
|
||||||
|
expect(formatLocalDateOnly(wrapper.emitted("update:endDate")?.at(-1)?.[0])).toBe("2026-04-30");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import { nextTick } from "vue";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { mountWithApp } from "./helpers/mountWithApp.js";
|
||||||
|
|
||||||
|
const sessionMocks = vi.hoisted(() => ({
|
||||||
|
addVehicle: vi.fn(),
|
||||||
|
parseErrorMessage: vi.fn(),
|
||||||
|
vehicleTypeOptions: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/session/token/SessionUser.vue", () => {
|
||||||
|
const sessionUser = {
|
||||||
|
functions: {
|
||||||
|
parseErrorMessage: sessionMocks.parseErrorMessage,
|
||||||
|
},
|
||||||
|
objects: {
|
||||||
|
vehicles: {
|
||||||
|
add: sessionMocks.addVehicle,
|
||||||
|
columns: {
|
||||||
|
type: {
|
||||||
|
options: sessionMocks.vehicleTypeOptions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
SessionUser: sessionUser,
|
||||||
|
default: sessionUser,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import SuperuserAddVehicleModal from "@/components/forms/superUser/SuperuserAddVehicleModal.vue";
|
||||||
|
|
||||||
|
const CustomerSearchSelectStub = {
|
||||||
|
props: ["modelValue"],
|
||||||
|
emits: ["update:modelValue"],
|
||||||
|
template: `
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="customer-select-stub"
|
||||||
|
@click="$emit('update:modelValue', { customerNumber: 12345679, name: 'Acme Transport' })"
|
||||||
|
>
|
||||||
|
Select customer
|
||||||
|
</button>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
en: {
|
||||||
|
vehicles: {
|
||||||
|
add_modal: {
|
||||||
|
customer_label: "Customer",
|
||||||
|
customer_placeholder: "Search customers",
|
||||||
|
error: "Unable to add vehicle.",
|
||||||
|
reference_placeholder: "Optional reference",
|
||||||
|
registration_placeholder: "Registration number",
|
||||||
|
selected_customer: "Selected customer",
|
||||||
|
submit: "Add vehicle",
|
||||||
|
title: "Add vehicle",
|
||||||
|
type_load_error: "Unable to load vehicle types.",
|
||||||
|
type_placeholder: "Select vehicle type",
|
||||||
|
validation_error: "Select required fields.",
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
license_plate: "Registration",
|
||||||
|
type: "Type",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
objects: {
|
||||||
|
vehicles: {
|
||||||
|
columns: {
|
||||||
|
wash_subscription: "Wash subscription",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
cancel: "Cancel",
|
||||||
|
close: "Close",
|
||||||
|
reference: "Reference",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const flushAll = async () => {
|
||||||
|
await Promise.resolve();
|
||||||
|
await nextTick();
|
||||||
|
await Promise.resolve();
|
||||||
|
await nextTick();
|
||||||
|
};
|
||||||
|
|
||||||
|
const mountComponent = () =>
|
||||||
|
mountWithApp(SuperuserAddVehicleModal, {
|
||||||
|
messages,
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
CustomerSearchSelect: CustomerSearchSelectStub,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("SuperuserAddVehicleModal", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
sessionMocks.vehicleTypeOptions.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 53,
|
||||||
|
name: "Forvogn",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
sessionMocks.addVehicle.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
id: 7002,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
sessionMocks.parseErrorMessage.mockReturnValue(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("submits the selected customer and vehicle fields through the vehicle API", async () => {
|
||||||
|
const wrapper = mountComponent();
|
||||||
|
await flushAll();
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="customer-select-stub"]').trigger("click");
|
||||||
|
await wrapper.get('[data-testid="superuser-add-vehicle-registration"]').setValue("ab12345");
|
||||||
|
await wrapper.get('[data-testid="superuser-add-vehicle-type"]').setValue("53");
|
||||||
|
await wrapper.get('[data-testid="superuser-add-vehicle-wash-subscription"]').setValue(true);
|
||||||
|
await wrapper.get('[data-testid="superuser-add-vehicle-reference"]').setValue("Fleet ref");
|
||||||
|
await wrapper.get('[data-testid="superuser-add-vehicle-submit"]').trigger("click");
|
||||||
|
await flushAll();
|
||||||
|
|
||||||
|
expect(sessionMocks.addVehicle).toHaveBeenCalledWith(53, "AB12345", true, 12345679, "Fleet ref");
|
||||||
|
expect(wrapper.emitted("created")).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps submit disabled until required fields are present", async () => {
|
||||||
|
const wrapper = mountComponent();
|
||||||
|
await flushAll();
|
||||||
|
|
||||||
|
expect(wrapper.get('[data-testid="superuser-add-vehicle-submit"]').attributes("disabled")).toBeDefined();
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="customer-select-stub"]').trigger("click");
|
||||||
|
await wrapper.get('[data-testid="superuser-add-vehicle-registration"]').setValue("AB12345");
|
||||||
|
await wrapper.get('[data-testid="superuser-add-vehicle-type"]').setValue("53");
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.get('[data-testid="superuser-add-vehicle-submit"]').attributes("disabled")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -367,6 +367,8 @@ describe("Invoice orders pagination contract", () => {
|
|||||||
expect(invoiceOrdersPaginationSource).toContain(
|
expect(invoiceOrdersPaginationSource).toContain(
|
||||||
"const onDateRangeSelected = (newSelectionStartDate, newSelectionToDate) => {"
|
"const onDateRangeSelected = (newSelectionStartDate, newSelectionToDate) => {"
|
||||||
);
|
);
|
||||||
|
expect(invoiceOrdersPaginationSource).toContain("formatLocalDateOnly(newSelectionStartDate)");
|
||||||
|
expect(invoiceOrdersPaginationSource).toContain("formatLocalDateOnly(newSelectionToDate)");
|
||||||
expect(invoiceOrdersPaginationSource).toContain("date_from.value = formattedStartDate;");
|
expect(invoiceOrdersPaginationSource).toContain("date_from.value = formattedStartDate;");
|
||||||
expect(invoiceOrdersPaginationSource).toContain("date_to.value = formattedEndDate;");
|
expect(invoiceOrdersPaginationSource).toContain("date_to.value = formattedEndDate;");
|
||||||
expect(invoiceOrdersPaginationSource).toContain('setFilter("created_at-date_from", formattedStartDate, true);');
|
expect(invoiceOrdersPaginationSource).toContain('setFilter("created_at-date_from", formattedStartDate, true);');
|
||||||
|
|||||||
Reference in New Issue
Block a user