Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c26da10677 |
@@ -52,10 +52,6 @@ loadList();
|
||||
:columnLabels="{ type: SessionUser.objects.vehicles.columns.type.label }"
|
||||
/>
|
||||
</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 -->
|
||||
<div class="vehicles-pagination__add-action"
|
||||
v-if="router.currentRoute.value.path.startsWith('/user')">
|
||||
|
||||
@@ -12,6 +12,11 @@ import UserOtherVaskeabonnement
|
||||
import Swal from "sweetalert2";
|
||||
import OrderItemsTable from "@/components/displays/department/pos/order/orderItemsTable.vue";
|
||||
import OrderContentTable from "@/components/displays/superuser/tables/OrderContentTable.vue";
|
||||
import {
|
||||
buildMultiMonthInvoiceContext,
|
||||
MULTI_MONTH_INVOICE_ACTION,
|
||||
promptMultiMonthInvoiceWarning,
|
||||
} from "@/services/invoiceMonthSplitWarning.js";
|
||||
|
||||
const props = defineProps({
|
||||
orders: {
|
||||
@@ -126,32 +131,34 @@ const isAnyOrderSelected = () => {
|
||||
return selectedInvoiceCollections.value.length > 0;
|
||||
}
|
||||
|
||||
const getSelectedOrders = () => {
|
||||
return props.orders.filter((order) => selectedInvoiceCollections.value.includes(order.invoice_collection_id));
|
||||
}
|
||||
|
||||
/** Invoice collections */
|
||||
const onInvoiceCollections = async () => {
|
||||
// Check if any orders are selected
|
||||
if (selectedInvoiceCollections.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
/** Create the invoice */
|
||||
const onCreateInvoiceDraft = async () => {
|
||||
// Create the invoice
|
||||
console.log('Create invoice');
|
||||
await SessionUser.objects.collectedOrderInvoices.functions.economic.invoice(parseInt(props.collectedOrderInvoice.id)).then((response) => {
|
||||
console.log('Invoice created successfully', response);
|
||||
Swal.fire({
|
||||
title: 'Fakturaen er oprettet',
|
||||
text: 'Fakturaen er oprettet i E-conomic',
|
||||
icon: 'success',
|
||||
showConfirmButton: false,
|
||||
timer: 2000
|
||||
}).then(() => {
|
||||
location.reload();
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.log('Error creating invoice', error);
|
||||
}
|
||||
)
|
||||
const selectedOrders = getSelectedOrders();
|
||||
const invoiceWarningContext = buildMultiMonthInvoiceContext(selectedOrders, {
|
||||
getDate: (order) => order?.created_at ?? order?.date,
|
||||
getInvoiceCollectionId: (order) => order?.invoice_collection_id,
|
||||
});
|
||||
const invoiceWarningAction = await promptMultiMonthInvoiceWarning({
|
||||
context: invoiceWarningContext,
|
||||
splitByMonth: SessionUser.objects.collectedOrderInvoices.functions.split_by_month,
|
||||
parseErrorMessage: SessionUser.functions.parseErrorMessage,
|
||||
});
|
||||
if (invoiceWarningAction === MULTI_MONTH_INVOICE_ACTION.SPLIT) {
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
if (invoiceWarningAction === MULTI_MONTH_INVOICE_ACTION.CANCEL) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < selectedInvoiceCollections.value.length; i++) {
|
||||
const selectedInvoiceCollectionId = selectedInvoiceCollections.value[i];
|
||||
// Check if the invoice collection is already booked
|
||||
@@ -210,6 +217,7 @@ const isOrderContentVisible = (order) => {
|
||||
class="button is-small"
|
||||
@click="onInvoiceCollections()"
|
||||
:disabled="!isAnyOrderSelected()"
|
||||
data-testid="invoice-order-table-invoice-button"
|
||||
>
|
||||
{{ $t('global.invoice_now') }}
|
||||
</button>
|
||||
@@ -274,4 +282,4 @@ const isOrderContentVisible = (order) => {
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
<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>
|
||||
@@ -1,288 +0,0 @@
|
||||
<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>
|
||||
@@ -444,10 +444,14 @@ export const CollectedOrderInvoices = {
|
||||
});
|
||||
},
|
||||
split_by_month: async (dateFrom, dateTo, options = {}) => {
|
||||
const invoiceCollectionIds = Array.isArray(options.invoiceCollectionIds)
|
||||
? options.invoiceCollectionIds
|
||||
: options.invoice_collection_ids;
|
||||
return authenticatedRequest('/collected-invoices/split-by-month', 'POST', {
|
||||
dateFrom,
|
||||
dateTo,
|
||||
...(options.preview !== undefined ? { preview: !!options.preview } : {}),
|
||||
...(Array.isArray(invoiceCollectionIds) ? { invoice_collection_ids: invoiceCollectionIds } : {}),
|
||||
}).then((response) => {
|
||||
console.log(response);
|
||||
return response;
|
||||
|
||||
@@ -4028,6 +4028,15 @@
|
||||
"preview_title": "@:{'words.generated.forhandsvis'} @:{'words.generated.manedsopdeling'}",
|
||||
"success_text": "@:{'words.generated.behandlede'} {processed} @:{'words.generated.fakturasamlinger'}. Opdelte {changed} @:{'words.generated.og'} sprang {skipped} @:{'words.generated.over'}.",
|
||||
"success_title": "@.capitalize:{'words.generated.manedsopdeling'} @:{'words.generated.fuldført'}"
|
||||
},
|
||||
"multi_month_invoice_warning": {
|
||||
"invoice_together": "Fakturer sammen",
|
||||
"split_by_month": "Opdel efter måned",
|
||||
"split_error_title": "Månedsopdeling mislykkedes",
|
||||
"split_success_text": "Behandlede {processed} fakturasamlinger. Opdelte {changed} og sprang {skipped} over.",
|
||||
"split_success_title": "Månedsopdeling fuldført",
|
||||
"text": "Du er ved at fakturere ordrer fra flere måneder sammen ({months}). Skal de i stedet opdeles efter måned?",
|
||||
"title": "Ordrer fra flere måneder"
|
||||
}
|
||||
},
|
||||
"invoicing": {
|
||||
@@ -5932,20 +5941,6 @@
|
||||
},
|
||||
"vehicles": {
|
||||
"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'}",
|
||||
"color": "Farve",
|
||||
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
|
||||
|
||||
@@ -4139,6 +4139,15 @@
|
||||
"preview_title": "Preview @:{'words.generated.monthly'} @:{'words.generated.split'}",
|
||||
"success_text": "@:{'words.generated.processed'} {processed} @:{'words.generated.invoice'} @:{'words.generated.collections'}. @.capitalize:{'words.generated.split'} {changed}, @:{'words.generated.skipped'} {skipped}.",
|
||||
"success_title": "@.capitalize:{'words.generated.monthly'} @:{'words.generated.split'} @:{'words.generated.completed'}"
|
||||
},
|
||||
"multi_month_invoice_warning": {
|
||||
"invoice_together": "Zusammen abrechnen",
|
||||
"split_by_month": "Nach Monat aufteilen",
|
||||
"split_error_title": "Monatsaufteilung fehlgeschlagen",
|
||||
"split_success_text": "{processed} Rechnungssammlungen verarbeitet. {changed} aufgeteilt, {skipped} übersprungen.",
|
||||
"split_success_title": "Monatsaufteilung abgeschlossen",
|
||||
"text": "Sie sind dabei, Aufträge aus mehreren Monaten gemeinsam abzurechnen ({months}). Sollen sie stattdessen nach Monat aufgeteilt werden?",
|
||||
"title": "Aufträge aus mehreren Monaten"
|
||||
}
|
||||
},
|
||||
"invoicing": {
|
||||
|
||||
@@ -3860,6 +3860,15 @@
|
||||
"preview_title": "@.capitalize:{'words.generated.preview'} @:{'words.generated.monthly'} @:{'words.generated.split'}",
|
||||
"success_text": "@:{'words.generated.processed'} {processed} @:{'words.generated.invoice'} @:{'words.generated.collections'}. @.capitalize:{'words.generated.split'} {changed}, @:{'words.generated.skipped'} {skipped}.",
|
||||
"success_title": "@.capitalize:{'words.generated.monthly'} @:{'words.generated.split'} @:{'words.generated.completed'}"
|
||||
},
|
||||
"multi_month_invoice_warning": {
|
||||
"invoice_together": "Invoice together",
|
||||
"split_by_month": "Split by month",
|
||||
"split_error_title": "Monthly split failed",
|
||||
"split_success_text": "Processed {processed} invoice collections. Split {changed}, skipped {skipped}.",
|
||||
"split_success_title": "Monthly split completed",
|
||||
"text": "You are about to invoice orders from multiple months together ({months}). Should they be split by month instead?",
|
||||
"title": "Orders from multiple months"
|
||||
}
|
||||
},
|
||||
"invoicing": {
|
||||
@@ -5764,20 +5773,6 @@
|
||||
},
|
||||
"vehicles": {
|
||||
"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",
|
||||
"color": "Color",
|
||||
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
|
||||
|
||||
@@ -3149,6 +3149,15 @@
|
||||
"preview_title": "@:{'templates.generated.compat.invoicing_period.monthly_split.preview_title'}",
|
||||
"success_text": "@:{'templates.generated.compat.invoicing_period.monthly_split.success_text'}",
|
||||
"success_title": "@:{'templates.generated.compat.invoicing_period.monthly_split.success_title'}"
|
||||
},
|
||||
"multi_month_invoice_warning": {
|
||||
"invoice_together": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.invoice_together'}",
|
||||
"split_by_month": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.split_by_month'}",
|
||||
"split_error_title": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.split_error_title'}",
|
||||
"split_success_text": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.split_success_text'}",
|
||||
"split_success_title": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.split_success_title'}",
|
||||
"text": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.text'}",
|
||||
"title": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.title'}"
|
||||
}
|
||||
},
|
||||
"invoicing": {
|
||||
@@ -5834,20 +5843,6 @@
|
||||
"vehicles": {
|
||||
"actions": "@:common.actions",
|
||||
"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'}",
|
||||
"color": "@:{'templates.generated.compat.vehicles.color'}",
|
||||
"created_at": "@:{'templates.generated.compat.global.generated'}",
|
||||
|
||||
@@ -4142,6 +4142,15 @@
|
||||
"preview_title": "Preview @:{'words.generated.monthly'} @:{'words.generated.split'}",
|
||||
"success_text": "@:{'words.generated.processed'} {processed} @:{'words.generated.invoice'} @:{'words.generated.collections'}. @.capitalize:{'words.generated.split'} {changed}, @:{'words.generated.skipped'} {skipped}.",
|
||||
"success_title": "@.capitalize:{'words.generated.monthly'} @:{'words.generated.split'} @:{'words.generated.completed'}"
|
||||
},
|
||||
"multi_month_invoice_warning": {
|
||||
"invoice_together": "Fakturer samlet",
|
||||
"split_by_month": "Del opp etter måned",
|
||||
"split_error_title": "Månedsdeling mislyktes",
|
||||
"split_success_text": "Behandlet {processed} fakturasamlinger. Delte opp {changed}, hoppet over {skipped}.",
|
||||
"split_success_title": "Månedsdeling fullført",
|
||||
"text": "Du er i ferd med å fakturere ordrer fra flere måneder samlet ({months}). Skal de i stedet deles opp etter måned?",
|
||||
"title": "Ordrer fra flere måneder"
|
||||
}
|
||||
},
|
||||
"invoicing": {
|
||||
|
||||
@@ -4192,6 +4192,15 @@
|
||||
"preview_title": "Preview @:{'words.generated.monthly'} @:{'words.generated.split'}",
|
||||
"success_text": "@:{'words.generated.processed'} {processed} @:{'words.generated.invoice'} @:{'words.generated.collections'}. @.capitalize:{'words.generated.split'} {changed}, @:{'words.generated.skipped'} {skipped}.",
|
||||
"success_title": "@.capitalize:{'words.generated.monthly'} @:{'words.generated.split'} @:{'words.generated.completed'}"
|
||||
},
|
||||
"multi_month_invoice_warning": {
|
||||
"invoice_together": "Fakturera tillsammans",
|
||||
"split_by_month": "Dela upp per månad",
|
||||
"split_error_title": "Månadsuppdelning misslyckades",
|
||||
"split_success_text": "Bearbetade {processed} fakturasamlingar. Delade upp {changed}, hoppade över {skipped}.",
|
||||
"split_success_title": "Månadsuppdelning klar",
|
||||
"text": "Du håller på att fakturera ordrar från flera månader tillsammans ({months}). Ska de delas upp per månad i stället?",
|
||||
"title": "Ordrar från flera månader"
|
||||
}
|
||||
},
|
||||
"invoicing": {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compat": {
|
||||
"invoicing_period": {
|
||||
"multi_month_invoice_warning": {
|
||||
"invoice_together": "Fakturer sammen",
|
||||
"split_by_month": "Opdel efter måned",
|
||||
"split_error_title": "Månedsopdeling mislykkedes",
|
||||
"split_success_text": "Behandlede {processed} fakturasamlinger. Opdelte {changed} og sprang {skipped} over.",
|
||||
"split_success_title": "Månedsopdeling fuldført",
|
||||
"text": "Du er ved at fakturere ordrer fra flere måneder sammen ({months}). Skal de i stedet opdeles efter måned?",
|
||||
"title": "Ordrer fra flere måneder"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,6 @@
|
||||
"compat": {
|
||||
"vehicles": {
|
||||
"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'}",
|
||||
"color": "Farve",
|
||||
"delete_vehicle": "@:{'phrases.compat.vehicles.delete'}",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compat": {
|
||||
"invoicing_period": {
|
||||
"multi_month_invoice_warning": {
|
||||
"invoice_together": "Zusammen abrechnen",
|
||||
"split_by_month": "Nach Monat aufteilen",
|
||||
"split_error_title": "Monatsaufteilung fehlgeschlagen",
|
||||
"split_success_text": "{processed} Rechnungssammlungen verarbeitet. {changed} aufgeteilt, {skipped} übersprungen.",
|
||||
"split_success_title": "Monatsaufteilung abgeschlossen",
|
||||
"text": "Sie sind dabei, Aufträge aus mehreren Monaten gemeinsam abzurechnen ({months}). Sollen sie stattdessen nach Monat aufgeteilt werden?",
|
||||
"title": "Aufträge aus mehreren Monaten"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compat": {
|
||||
"invoicing_period": {
|
||||
"multi_month_invoice_warning": {
|
||||
"invoice_together": "Invoice together",
|
||||
"split_by_month": "Split by month",
|
||||
"split_error_title": "Monthly split failed",
|
||||
"split_success_text": "Processed {processed} invoice collections. Split {changed}, skipped {skipped}.",
|
||||
"split_success_title": "Monthly split completed",
|
||||
"text": "You are about to invoice orders from multiple months together ({months}). Should they be split by month instead?",
|
||||
"title": "Orders from multiple months"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,6 @@
|
||||
"compat": {
|
||||
"vehicles": {
|
||||
"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",
|
||||
"color": "Color",
|
||||
"delete_vehicle": "@:{'phrases.compat.vehicles.delete'}",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"invoicing_period": {
|
||||
"multi_month_invoice_warning": {
|
||||
"invoice_together": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.invoice_together'}",
|
||||
"split_by_month": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.split_by_month'}",
|
||||
"split_error_title": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.split_error_title'}",
|
||||
"split_success_text": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.split_success_text'}",
|
||||
"split_success_title": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.split_success_title'}",
|
||||
"text": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.text'}",
|
||||
"title": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.title'}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,6 @@
|
||||
"vehicles": {
|
||||
"actions": "@:common.actions",
|
||||
"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'}",
|
||||
"color": "@:{'phrases.compat.vehicles.color'}",
|
||||
"created_at": "@:{'phrases.compat.global.generated'}",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compat": {
|
||||
"invoicing_period": {
|
||||
"multi_month_invoice_warning": {
|
||||
"invoice_together": "Fakturer samlet",
|
||||
"split_by_month": "Del opp etter måned",
|
||||
"split_error_title": "Månedsdeling mislyktes",
|
||||
"split_success_text": "Behandlet {processed} fakturasamlinger. Delte opp {changed}, hoppet over {skipped}.",
|
||||
"split_success_title": "Månedsdeling fullført",
|
||||
"text": "Du er i ferd med å fakturere ordrer fra flere måneder samlet ({months}). Skal de i stedet deles opp etter måned?",
|
||||
"title": "Ordrer fra flere måneder"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compat": {
|
||||
"invoicing_period": {
|
||||
"multi_month_invoice_warning": {
|
||||
"invoice_together": "Fakturera tillsammans",
|
||||
"split_by_month": "Dela upp per månad",
|
||||
"split_error_title": "Månadsuppdelning misslyckades",
|
||||
"split_success_text": "Bearbetade {processed} fakturasamlingar. Delade upp {changed}, hoppade över {skipped}.",
|
||||
"split_success_title": "Månadsuppdelning klar",
|
||||
"text": "Du håller på att fakturera ordrar från flera månader tillsammans ({months}). Ska de delas upp per månad i stället?",
|
||||
"title": "Ordrar från flera månader"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import Swal from "sweetalert2";
|
||||
import i18n from "@/i18n";
|
||||
|
||||
export const MULTI_MONTH_INVOICE_ACTION = {
|
||||
CONTINUE: "continue",
|
||||
CANCEL: "cancel",
|
||||
SPLIT: "split",
|
||||
};
|
||||
|
||||
const toPositiveInteger = (value) => {
|
||||
const parsed = Number.parseInt(String(value ?? ""), 10);
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
||||
};
|
||||
|
||||
const getDateKey = (value) => {
|
||||
const rawValue = String(value ?? "").trim();
|
||||
const directMatch = rawValue.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (directMatch) {
|
||||
return `${directMatch[1]}-${directMatch[2]}-${directMatch[3]}`;
|
||||
}
|
||||
|
||||
const parsedDate = new Date(rawValue);
|
||||
if (Number.isNaN(parsedDate.getTime())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const year = parsedDate.getFullYear();
|
||||
const month = String(parsedDate.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(parsedDate.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
export const getMonthKey = (value) => {
|
||||
const dateKey = getDateKey(value);
|
||||
return dateKey ? dateKey.slice(0, 7) : null;
|
||||
};
|
||||
|
||||
const translate = (key, params = {}) => i18n.global.t(key, params);
|
||||
|
||||
const getSplitResponsePayload = (response = {}) => response?.data?.data ?? response?.data ?? {};
|
||||
|
||||
export const buildMultiMonthInvoiceContext = (
|
||||
items = [],
|
||||
{
|
||||
getDate = (item) => item?.created_at ?? item?.date,
|
||||
getInvoiceCollectionId = (item) => item?.invoice_collection_id,
|
||||
} = {}
|
||||
) => {
|
||||
const months = new Set();
|
||||
const dateKeys = [];
|
||||
const invoiceCollectionIds = new Set();
|
||||
|
||||
(Array.isArray(items) ? items : []).forEach((item) => {
|
||||
const dateValue = getDate(item);
|
||||
const dateKey = getDateKey(dateValue);
|
||||
if (dateKey) {
|
||||
dateKeys.push(dateKey);
|
||||
months.add(dateKey.slice(0, 7));
|
||||
}
|
||||
|
||||
const invoiceCollectionId = toPositiveInteger(getInvoiceCollectionId(item));
|
||||
if (invoiceCollectionId) {
|
||||
invoiceCollectionIds.add(invoiceCollectionId);
|
||||
}
|
||||
});
|
||||
|
||||
dateKeys.sort();
|
||||
|
||||
return {
|
||||
months: Array.from(months).sort(),
|
||||
dateFrom: dateKeys[0] ?? null,
|
||||
dateTo: dateKeys[dateKeys.length - 1] ?? null,
|
||||
invoiceCollectionIds: Array.from(invoiceCollectionIds).sort((left, right) => left - right),
|
||||
};
|
||||
};
|
||||
|
||||
export const shouldWarnAboutMultiMonthInvoice = (context = {}) => (
|
||||
Array.isArray(context.months) &&
|
||||
context.months.length > 1 &&
|
||||
Array.isArray(context.invoiceCollectionIds) &&
|
||||
context.invoiceCollectionIds.length > 0 &&
|
||||
Boolean(context.dateFrom) &&
|
||||
Boolean(context.dateTo)
|
||||
);
|
||||
|
||||
export const promptMultiMonthInvoiceWarning = async ({
|
||||
context,
|
||||
splitByMonth,
|
||||
parseErrorMessage = (error) => error?.message ?? String(error),
|
||||
} = {}) => {
|
||||
if (!shouldWarnAboutMultiMonthInvoice(context)) {
|
||||
return MULTI_MONTH_INVOICE_ACTION.CONTINUE;
|
||||
}
|
||||
|
||||
const months = context.months.join(", ");
|
||||
const confirmation = await Swal.fire({
|
||||
icon: "warning",
|
||||
title: translate("invoicing_period.multi_month_invoice_warning.title"),
|
||||
text: translate("invoicing_period.multi_month_invoice_warning.text", { months }),
|
||||
showCancelButton: true,
|
||||
showDenyButton: true,
|
||||
confirmButtonText: translate("invoicing_period.multi_month_invoice_warning.split_by_month"),
|
||||
denyButtonText: translate("invoicing_period.multi_month_invoice_warning.invoice_together"),
|
||||
cancelButtonText: translate("common.cancel"),
|
||||
});
|
||||
|
||||
if (confirmation.isDenied) {
|
||||
return MULTI_MONTH_INVOICE_ACTION.CONTINUE;
|
||||
}
|
||||
|
||||
if (!confirmation.isConfirmed) {
|
||||
return MULTI_MONTH_INVOICE_ACTION.CANCEL;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await splitByMonth(context.dateFrom, context.dateTo, {
|
||||
invoiceCollectionIds: context.invoiceCollectionIds,
|
||||
preview: false,
|
||||
});
|
||||
const result = getSplitResponsePayload(response);
|
||||
await Swal.fire({
|
||||
icon: "success",
|
||||
title: translate("invoicing_period.multi_month_invoice_warning.split_success_title"),
|
||||
text: translate("invoicing_period.multi_month_invoice_warning.split_success_text", {
|
||||
processed: result.processed_count ?? 0,
|
||||
changed: result.changed_count ?? 0,
|
||||
skipped: result.skipped_count ?? 0,
|
||||
}),
|
||||
});
|
||||
return MULTI_MONTH_INVOICE_ACTION.SPLIT;
|
||||
} catch (error) {
|
||||
await Swal.fire({
|
||||
icon: "error",
|
||||
title: translate("invoicing_period.multi_month_invoice_warning.split_error_title"),
|
||||
text: parseErrorMessage(error),
|
||||
});
|
||||
return MULTI_MONTH_INVOICE_ACTION.CANCEL;
|
||||
}
|
||||
};
|
||||
+36
-4
@@ -22,6 +22,11 @@ import InvoicingBillingPeriodFilters from "@/views/dashboards/superUserDashboard
|
||||
import InvoicingBillingPeriodCustomerAttributes from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/InvoicingBillingPeriodCustomerAttributes.vue";
|
||||
import InvoicingPeriodFlagList from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodFlagList.vue";
|
||||
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||
import {
|
||||
buildMultiMonthInvoiceContext,
|
||||
MULTI_MONTH_INVOICE_ACTION,
|
||||
promptMultiMonthInvoiceWarning,
|
||||
} from "@/services/invoiceMonthSplitWarning.js";
|
||||
import {
|
||||
buildPossibleDuplicateGroups,
|
||||
formatDuplicateDateLabel,
|
||||
@@ -330,6 +335,13 @@ const parsePositiveInteger = (value: any) => {
|
||||
|
||||
const getCustomerNumber = (customer: any) => parsePositiveInteger(customer?.customer_number);
|
||||
|
||||
const markCustomerPeriodRefreshLoading = (customer: any) => {
|
||||
const customerNumber = getCustomerNumber(customer);
|
||||
if (customerNumber) {
|
||||
invoiceQueue.markPeriodRefreshLoading?.([customerNumber], []);
|
||||
}
|
||||
};
|
||||
|
||||
const queueInvoiceCollections = (invoiceCollectionIds: number[], customer: any = null) => {
|
||||
const uniqueInvoiceCollectionIds = Array.from(
|
||||
new Set(
|
||||
@@ -351,12 +363,10 @@ const queueInvoiceCollections = (invoiceCollectionIds: number[], customer: any =
|
||||
|
||||
const onClickInvoiceNow = async (customer: any, transactionIds: number[]) => {
|
||||
const customerNumber = getCustomerNumber(customer);
|
||||
if (customerNumber) {
|
||||
invoiceQueue.markPeriodRefreshLoading?.([customerNumber], []);
|
||||
}
|
||||
|
||||
try {
|
||||
if (transactionIds.length === 0) {
|
||||
markCustomerPeriodRefreshLoading(customer);
|
||||
const month = dates.variables.start.value.getMonth() + 1;
|
||||
const year = dates.variables.start.value.getFullYear();
|
||||
|
||||
@@ -392,10 +402,32 @@ const onClickInvoiceNow = async (customer: any, transactionIds: number[]) => {
|
||||
await fetchMissingInvoiceCollections(customer, transactionIds);
|
||||
const invoiceCollectionIds = getInvoiceCollectionIdsForTransactionIds(customer, transactionIds);
|
||||
if (invoiceCollectionIds.length === 0) {
|
||||
invoiceQueue.finishPeriodRefresh?.([customerNumber], []);
|
||||
return;
|
||||
}
|
||||
|
||||
const invoiceWarningContext = buildMultiMonthInvoiceContext(
|
||||
transactionIds
|
||||
.map((transactionId) => getTransactionById(customer, transactionId))
|
||||
.filter((transaction: any) => transaction !== null),
|
||||
{
|
||||
getDate: (transaction: any) => transaction?.date ?? transaction?.created_at,
|
||||
getInvoiceCollectionId: (transaction: any) => getTransactionInvoiceCollectionId(transaction),
|
||||
}
|
||||
);
|
||||
const invoiceWarningAction = await promptMultiMonthInvoiceWarning({
|
||||
context: invoiceWarningContext,
|
||||
splitByMonth: SessionUser.objects.collectedOrderInvoices.functions.split_by_month,
|
||||
parseErrorMessage: SessionUser.functions.parseErrorMessage,
|
||||
});
|
||||
if (invoiceWarningAction === MULTI_MONTH_INVOICE_ACTION.SPLIT) {
|
||||
reloadPeriodPage();
|
||||
return;
|
||||
}
|
||||
if (invoiceWarningAction === MULTI_MONTH_INVOICE_ACTION.CANCEL) {
|
||||
return;
|
||||
}
|
||||
|
||||
markCustomerPeriodRefreshLoading(customer);
|
||||
queueInvoiceCollections(invoiceCollectionIds, customer);
|
||||
} catch (error: any) {
|
||||
invoiceQueue.finishPeriodRefresh?.([customerNumber], []);
|
||||
|
||||
@@ -1,53 +1,21 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { getOrders } from "@/components/shop/Orders.vue";
|
||||
import { showCreateOrderForm } from "@/components/forms/superUser/createOrderForm.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 { SessionUser } from "@/components/session/token/SessionUser.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>
|
||||
|
||||
<template>
|
||||
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessSuperUser()">
|
||||
<SuperUserDashboardNavigation />
|
||||
<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"
|
||||
/>
|
||||
<VehiclesPagination />
|
||||
</RestrictedPageWrapper>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.superuser-vehicles__add-button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
</style>
|
||||
@@ -1933,6 +1933,111 @@ test.describe("Invoicing period tab", () => {
|
||||
await expect(page).toHaveURL(/activeTab=period/);
|
||||
});
|
||||
|
||||
test("@smoke period view warns and splits selected multi-month invoice collections", async ({ page }) => {
|
||||
const splitRequests = [];
|
||||
const economicInvoiceRequests = [];
|
||||
await openPeriodView(page, {
|
||||
payloadFactory: () => ({
|
||||
types: {
|
||||
all: [
|
||||
{
|
||||
id: 31,
|
||||
customer_number: 4301,
|
||||
customer_name: "Multi Month Fleet",
|
||||
requires_action: true,
|
||||
transactions: [
|
||||
{
|
||||
id: 8801,
|
||||
date: "2026-03-28T10:00:00.000Z",
|
||||
amount: 120,
|
||||
booked: false,
|
||||
excluded: false,
|
||||
invoice_collection_id: 88001,
|
||||
},
|
||||
{
|
||||
id: 8802,
|
||||
date: "2026-04-02T10:00:00.000Z",
|
||||
amount: 180,
|
||||
booked: false,
|
||||
excluded: false,
|
||||
invoice_collection_id: 88001,
|
||||
},
|
||||
],
|
||||
queue: {
|
||||
has_active_job: false,
|
||||
statuses: [],
|
||||
invoice_collection_ids: [],
|
||||
is_action_blocked: false,
|
||||
},
|
||||
meta: {},
|
||||
},
|
||||
],
|
||||
invoice_per_order: [],
|
||||
fixed_pricing: [],
|
||||
tank_cleaning: [],
|
||||
special_arrangements: [],
|
||||
vehicle_subscriptions: [],
|
||||
possible_duplicates: [],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await page.route("**/collected-invoices/split-by-month**", async (route) => {
|
||||
if (
|
||||
route.request().method() !== "POST" ||
|
||||
!matchesApiPath(route.request().url(), "/collected-invoices/split-by-month")
|
||||
) {
|
||||
await route.fallback();
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = JSON.parse(route.request().postData() || "{}");
|
||||
splitRequests.push(payload);
|
||||
await route.fulfill(
|
||||
json({
|
||||
data: {
|
||||
preview: false,
|
||||
processed_count: 1,
|
||||
changed_count: 1,
|
||||
skipped_count: 0,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
await page.route("**/collected-invoices/economic**", async (route) => {
|
||||
if (
|
||||
route.request().method() === "POST" &&
|
||||
matchesApiPath(route.request().url(), "/collected-invoices/economic")
|
||||
) {
|
||||
economicInvoiceRequests.push(JSON.parse(route.request().postData() || "{}"));
|
||||
await route.fulfill(json({ data: {} }));
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fallback();
|
||||
});
|
||||
|
||||
await page.getByTestId("invoicing-period-view-selector-all").click();
|
||||
await expect(page.getByTestId("invoicing-period-customer-4301")).toBeVisible();
|
||||
await page.getByTestId("invoicing-period-customer-invoice-4301").click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: /Orders from multiple months|Ordrer fra flere måneder/i })
|
||||
).toBeVisible();
|
||||
await page.getByRole("button", { name: /Split by month|Opdel efter måned/i }).click();
|
||||
|
||||
await expect.poll(() => splitRequests.length).toBe(1);
|
||||
expect(splitRequests[0]).toEqual({
|
||||
dateFrom: "2026-03-28",
|
||||
dateTo: "2026-04-02",
|
||||
invoice_collection_ids: [88001],
|
||||
preview: false,
|
||||
});
|
||||
expect(economicInvoiceRequests).toEqual([]);
|
||||
await expect(page.getByText(/Monthly split completed|Månedsopdeling fuldført/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test("@smoke period view invoices multiple customers with page refresh and independent loading", async ({ page }) => {
|
||||
const token = "superuser-period-parallel-token";
|
||||
const periodRequests = [];
|
||||
|
||||
@@ -7,13 +7,10 @@ async function primeSuperuserSession(page) {
|
||||
}
|
||||
|
||||
test.describe("Superuser vehicles smoke", () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
permissions: ["superuser", "user"],
|
||||
pos: true,
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
});
|
||||
@@ -26,51 +23,4 @@ test.describe("Superuser vehicles smoke", () => {
|
||||
await expect(page.locator("body")).toContainText(/registrerede|registered/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,7 +2736,6 @@ export function createPosFixture(overrides = {}) {
|
||||
last_order_id: 54518,
|
||||
},
|
||||
],
|
||||
nextVehicleId: 7002,
|
||||
unknownVehicles: [],
|
||||
orderBookings: [],
|
||||
bookingOrderAssignments: [],
|
||||
@@ -3279,30 +3278,6 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
|
||||
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") {
|
||||
await route.fulfill(json({ success: true, data: posFixture.unknownVehicles || [] }));
|
||||
return true;
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
// @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,123 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("sweetalert2", () => ({
|
||||
default: {
|
||||
fire: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import Swal from "sweetalert2";
|
||||
import {
|
||||
buildMultiMonthInvoiceContext,
|
||||
MULTI_MONTH_INVOICE_ACTION,
|
||||
promptMultiMonthInvoiceWarning,
|
||||
shouldWarnAboutMultiMonthInvoice,
|
||||
} from "@/services/invoiceMonthSplitWarning.js";
|
||||
|
||||
describe("invoice month split warning", () => {
|
||||
beforeEach(() => {
|
||||
Swal.fire.mockReset();
|
||||
});
|
||||
|
||||
it("builds a scoped multi-month context without timezone shifting date strings", () => {
|
||||
const context = buildMultiMonthInvoiceContext([
|
||||
{ id: 1, created_at: "2026-03-31 23:30:00", invoice_collection_id: 501 },
|
||||
{ id: 2, created_at: "2026-04-01T00:30:00.000Z", invoice_collection_id: "501" },
|
||||
{ id: 3, created_at: "2026-04-03 09:00:00", invoice_collection_id: 502 },
|
||||
{ id: 4, created_at: "invalid", invoice_collection_id: null },
|
||||
]);
|
||||
|
||||
expect(context).toEqual({
|
||||
months: ["2026-03", "2026-04"],
|
||||
dateFrom: "2026-03-31",
|
||||
dateTo: "2026-04-03",
|
||||
invoiceCollectionIds: [501, 502],
|
||||
});
|
||||
expect(shouldWarnAboutMultiMonthInvoice(context)).toBe(true);
|
||||
});
|
||||
|
||||
it("continues without a modal for single-month selections", async () => {
|
||||
const action = await promptMultiMonthInvoiceWarning({
|
||||
context: buildMultiMonthInvoiceContext([
|
||||
{ created_at: "2026-04-01 10:00:00", invoice_collection_id: 501 },
|
||||
{ created_at: "2026-04-02 10:00:00", invoice_collection_id: 501 },
|
||||
]),
|
||||
splitByMonth: vi.fn(),
|
||||
});
|
||||
|
||||
expect(action).toBe(MULTI_MONTH_INVOICE_ACTION.CONTINUE);
|
||||
expect(Swal.fire).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("splits selected invoice collections when the warning is confirmed", async () => {
|
||||
const splitByMonth = vi.fn().mockResolvedValue({
|
||||
data: {
|
||||
data: {
|
||||
processed_count: 2,
|
||||
changed_count: 1,
|
||||
skipped_count: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
Swal.fire.mockResolvedValueOnce({ isConfirmed: true }).mockResolvedValueOnce({ isConfirmed: true });
|
||||
|
||||
const action = await promptMultiMonthInvoiceWarning({
|
||||
context: buildMultiMonthInvoiceContext([
|
||||
{ created_at: "2026-03-20 10:00:00", invoice_collection_id: 7001 },
|
||||
{ created_at: "2026-04-02 10:00:00", invoice_collection_id: 7001 },
|
||||
]),
|
||||
splitByMonth,
|
||||
});
|
||||
|
||||
expect(action).toBe(MULTI_MONTH_INVOICE_ACTION.SPLIT);
|
||||
expect(splitByMonth).toHaveBeenCalledWith("2026-03-20", "2026-04-02", {
|
||||
invoiceCollectionIds: [7001],
|
||||
preview: false,
|
||||
});
|
||||
expect(Swal.fire).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
icon: "warning",
|
||||
showDenyButton: true,
|
||||
})
|
||||
);
|
||||
expect(Swal.fire).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
icon: "success",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("continues invoicing together when the warning deny button is selected", async () => {
|
||||
const splitByMonth = vi.fn();
|
||||
Swal.fire.mockResolvedValueOnce({ isDenied: true });
|
||||
|
||||
const action = await promptMultiMonthInvoiceWarning({
|
||||
context: buildMultiMonthInvoiceContext([
|
||||
{ created_at: "2026-03-20 10:00:00", invoice_collection_id: 7001 },
|
||||
{ created_at: "2026-04-02 10:00:00", invoice_collection_id: 7001 },
|
||||
]),
|
||||
splitByMonth,
|
||||
});
|
||||
|
||||
expect(action).toBe(MULTI_MONTH_INVOICE_ACTION.CONTINUE);
|
||||
expect(splitByMonth).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cancels invoicing when the warning is dismissed", async () => {
|
||||
const splitByMonth = vi.fn();
|
||||
Swal.fire.mockResolvedValueOnce({ isDismissed: true });
|
||||
|
||||
const action = await promptMultiMonthInvoiceWarning({
|
||||
context: buildMultiMonthInvoiceContext([
|
||||
{ created_at: "2026-03-20 10:00:00", invoice_collection_id: 7001 },
|
||||
{ created_at: "2026-04-02 10:00:00", invoice_collection_id: 7001 },
|
||||
]),
|
||||
splitByMonth,
|
||||
});
|
||||
|
||||
expect(action).toBe(MULTI_MONTH_INVOICE_ACTION.CANCEL);
|
||||
expect(splitByMonth).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
// @vitest-environment jsdom
|
||||
import { flushPromises, mount } from "@vue/test-utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("vue-i18n", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: (key) => key,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("sweetalert2", () => ({
|
||||
default: {
|
||||
fire: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
||||
SessionUser: {
|
||||
objects: {
|
||||
global: {
|
||||
language: {
|
||||
hide_content: "Hide",
|
||||
show_content: "Show",
|
||||
status: "Status",
|
||||
completed: "Completed",
|
||||
not_completed: "Not completed",
|
||||
},
|
||||
},
|
||||
orders: {
|
||||
columns: {
|
||||
id: { label: "ID", visible: true },
|
||||
created_at: { label: "Created", visible: true },
|
||||
},
|
||||
},
|
||||
collectedOrderInvoices: {
|
||||
functions: {
|
||||
split_by_month: vi.fn(),
|
||||
economic: {
|
||||
invoice: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
vehicles: {
|
||||
columns: {
|
||||
wash_subscription: {
|
||||
label: "Subscription",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
functions: {
|
||||
currency: {
|
||||
toLocal: (value) => String(value),
|
||||
},
|
||||
parseErrorMessage: (error) => error?.message ?? String(error),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/views/dashboards/superUserDashboard/user/displays/other/UserOtherSpecialArrangement.vue", () => ({
|
||||
default: { template: "<div />" },
|
||||
}));
|
||||
|
||||
vi.mock("@/views/dashboards/superUserDashboard/user/displays/other/UserOtherVaskeabonnement.vue", () => ({
|
||||
default: { template: "<div />" },
|
||||
}));
|
||||
|
||||
vi.mock("@/components/displays/department/pos/order/orderItemsTable.vue", () => ({
|
||||
default: { template: "<div />" },
|
||||
}));
|
||||
|
||||
vi.mock("@/components/displays/superuser/tables/OrderContentTable.vue", () => ({
|
||||
default: { template: "<div />" },
|
||||
}));
|
||||
|
||||
import Swal from "sweetalert2";
|
||||
import InvoiceOrderTable from "@/components/displays/superuser/tables/InvoiceOrderTable.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
|
||||
const mountTable = (orders) =>
|
||||
mount(InvoiceOrderTable, {
|
||||
props: {
|
||||
orders,
|
||||
options: {},
|
||||
columns: {},
|
||||
user_id: 42,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) =>
|
||||
({
|
||||
"global.invoice_now": "Invoice now",
|
||||
"global.unselect": "Unselect",
|
||||
"common.all": "All",
|
||||
"common.select": "Select",
|
||||
}[key] ?? key),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe("InvoiceOrderTable multi-month warning", () => {
|
||||
beforeEach(() => {
|
||||
Swal.fire.mockReset();
|
||||
SessionUser.objects.collectedOrderInvoices.functions.split_by_month.mockReset();
|
||||
SessionUser.objects.collectedOrderInvoices.functions.economic.invoice.mockReset();
|
||||
SessionUser.objects.collectedOrderInvoices.functions.economic.invoice.mockResolvedValue({});
|
||||
});
|
||||
|
||||
it("warns for selected invoice collections containing orders from multiple months before invoicing together", async () => {
|
||||
Swal.fire.mockResolvedValueOnce({ isDenied: true }).mockReturnValueOnce(new Promise(() => {}));
|
||||
const wrapper = mountTable([
|
||||
{
|
||||
id: 1,
|
||||
invoice_collection_id: 9001,
|
||||
created_at: "2026-03-15 10:00:00",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
invoice_collection_id: 9001,
|
||||
created_at: "2026-04-02 10:00:00",
|
||||
},
|
||||
]);
|
||||
|
||||
await wrapper.find("tbody input[type='checkbox']").trigger("click");
|
||||
await wrapper.get("[data-testid='invoice-order-table-invoice-button']").trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(Swal.fire).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
icon: "warning",
|
||||
showDenyButton: true,
|
||||
})
|
||||
);
|
||||
expect(SessionUser.objects.collectedOrderInvoices.functions.split_by_month).not.toHaveBeenCalled();
|
||||
expect(SessionUser.objects.collectedOrderInvoices.functions.economic.invoice).toHaveBeenCalledWith(9001, 42);
|
||||
});
|
||||
|
||||
it("does not warn for selected invoice collections containing only one month", async () => {
|
||||
Swal.fire.mockReturnValueOnce(new Promise(() => {}));
|
||||
const wrapper = mountTable([
|
||||
{
|
||||
id: 1,
|
||||
invoice_collection_id: 9002,
|
||||
created_at: "2026-04-01 10:00:00",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
invoice_collection_id: 9002,
|
||||
created_at: "2026-04-02 10:00:00",
|
||||
},
|
||||
]);
|
||||
|
||||
await wrapper.find("tbody input[type='checkbox']").trigger("click");
|
||||
await wrapper.get("[data-testid='invoice-order-table-invoice-button']").trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(Swal.fire).toHaveBeenCalledTimes(1);
|
||||
expect(Swal.fire).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "Fakturaer oprettet",
|
||||
})
|
||||
);
|
||||
expect(SessionUser.objects.collectedOrderInvoices.functions.economic.invoice).toHaveBeenCalledWith(9002, 42);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment jsdom
|
||||
import { computed, nextTick } from "vue";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { flushPromises, mount } from "@vue/test-utils";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const {
|
||||
@@ -34,6 +34,12 @@ vi.mock("@/services/economicTransferQueue.js", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("sweetalert2", () => ({
|
||||
default: {
|
||||
fire: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
||||
SessionUser: {
|
||||
objects: {
|
||||
@@ -62,6 +68,7 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
||||
createVehicleSubscriptionInvoice: vi.fn(),
|
||||
add_fixed_pricing: vi.fn(),
|
||||
add_vehicle_subscriptions: vi.fn(),
|
||||
split_by_month: vi.fn(),
|
||||
},
|
||||
},
|
||||
vehicles: {
|
||||
@@ -76,6 +83,7 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
||||
currency: {
|
||||
toLocal: (value) => String(value),
|
||||
},
|
||||
parseErrorMessage: (error) => error?.message ?? String(error),
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -148,6 +156,7 @@ vi.mock(
|
||||
import InvoicingBillingPeriodViewAll from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { invoiceQueue } from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportInvoiceQueue.vue";
|
||||
import Swal from "sweetalert2";
|
||||
import {
|
||||
periodPaging,
|
||||
resetPeriodPagingState,
|
||||
@@ -222,11 +231,23 @@ describe("Invoicing period queue state", () => {
|
||||
loadingCustomerNumbersRef.value = [];
|
||||
SessionUser.objects.orders.get.multiple.mockReset();
|
||||
SessionUser.objects.orders.get.multiple.mockResolvedValue([]);
|
||||
SessionUser.objects.collectedOrderInvoices.functions.split_by_month.mockReset();
|
||||
SessionUser.objects.collectedOrderInvoices.functions.split_by_month.mockResolvedValue({
|
||||
data: {
|
||||
data: {
|
||||
processed_count: 1,
|
||||
changed_count: 1,
|
||||
skipped_count: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
invoiceQueue.addInvoiceCollectionsToQueue.mockReset();
|
||||
invoiceQueue.processInvoiceCollectionQueue.mockReset();
|
||||
invoiceQueue.markPeriodRefreshLoading.mockClear();
|
||||
invoiceQueue.finishPeriodRefresh.mockClear();
|
||||
invoiceQueue.isPeriodCustomerRefreshLoading.mockClear();
|
||||
Swal.fire.mockReset();
|
||||
Swal.fire.mockResolvedValue({ isDenied: true });
|
||||
resetPeriodPagingState();
|
||||
sharedVariablesRef.value = {
|
||||
types: {
|
||||
@@ -644,6 +665,168 @@ describe("Invoicing period queue state", () => {
|
||||
expect(windowOpenSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("splits instead of queueing when multi-month invoicing warning is confirmed", async () => {
|
||||
sharedVariablesRef.value = {
|
||||
types: {
|
||||
all: [
|
||||
{
|
||||
id: 8,
|
||||
customer_number: 1008,
|
||||
customer_name: "Multi Month Customer",
|
||||
requires_action: true,
|
||||
queue: {
|
||||
has_active_job: false,
|
||||
statuses: [],
|
||||
invoice_collection_ids: [],
|
||||
is_action_blocked: false,
|
||||
},
|
||||
transactions: [
|
||||
{
|
||||
id: 8101,
|
||||
amount: 75,
|
||||
booked: false,
|
||||
excluded: false,
|
||||
invoice_collection_id: 8100,
|
||||
date: "2026-03-28T10:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: 8102,
|
||||
amount: 125,
|
||||
booked: false,
|
||||
excluded: false,
|
||||
invoice_collection_id: 8100,
|
||||
date: "2026-04-02T10:00:00.000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
Swal.fire.mockResolvedValueOnce({ isConfirmed: true }).mockResolvedValueOnce({ isConfirmed: true });
|
||||
|
||||
const wrapper = mountView();
|
||||
await nextTick();
|
||||
|
||||
await wrapper.get("[data-testid='invoicing-period-customer-invoice-1008']").trigger("click");
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
|
||||
expect(SessionUser.objects.collectedOrderInvoices.functions.split_by_month).toHaveBeenCalledWith(
|
||||
"2026-03-28",
|
||||
"2026-04-02",
|
||||
{
|
||||
invoiceCollectionIds: [8100],
|
||||
preview: false,
|
||||
}
|
||||
);
|
||||
expect(invoiceQueue.addInvoiceCollectionsToQueue).not.toHaveBeenCalled();
|
||||
expect(invoiceQueue.processInvoiceCollectionQueue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("continues queueing together when multi-month invoicing warning is denied", async () => {
|
||||
sharedVariablesRef.value = {
|
||||
types: {
|
||||
all: [
|
||||
{
|
||||
id: 9,
|
||||
customer_number: 1009,
|
||||
customer_name: "Invoice Together Customer",
|
||||
requires_action: true,
|
||||
queue: {
|
||||
has_active_job: false,
|
||||
statuses: [],
|
||||
invoice_collection_ids: [],
|
||||
is_action_blocked: false,
|
||||
},
|
||||
transactions: [
|
||||
{
|
||||
id: 8201,
|
||||
amount: 75,
|
||||
booked: false,
|
||||
excluded: false,
|
||||
invoice_collection_id: 8200,
|
||||
date: "2026-03-28T10:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: 8202,
|
||||
amount: 125,
|
||||
booked: false,
|
||||
excluded: false,
|
||||
invoice_collection_id: 8200,
|
||||
date: "2026-04-02T10:00:00.000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
Swal.fire.mockResolvedValueOnce({ isDenied: true });
|
||||
|
||||
const wrapper = mountView();
|
||||
await nextTick();
|
||||
|
||||
await wrapper.get("[data-testid='invoicing-period-customer-invoice-1009']").trigger("click");
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
|
||||
expect(SessionUser.objects.collectedOrderInvoices.functions.split_by_month).not.toHaveBeenCalled();
|
||||
expect(invoiceQueue.addInvoiceCollectionsToQueue).toHaveBeenCalledWith([8200], {
|
||||
customerNumber: 1009,
|
||||
});
|
||||
expect(invoiceQueue.processInvoiceCollectionQueue).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not queue when multi-month invoicing warning is dismissed", async () => {
|
||||
sharedVariablesRef.value = {
|
||||
types: {
|
||||
all: [
|
||||
{
|
||||
id: 10,
|
||||
customer_number: 1010,
|
||||
customer_name: "Cancel Multi Month Customer",
|
||||
requires_action: true,
|
||||
queue: {
|
||||
has_active_job: false,
|
||||
statuses: [],
|
||||
invoice_collection_ids: [],
|
||||
is_action_blocked: false,
|
||||
},
|
||||
transactions: [
|
||||
{
|
||||
id: 8301,
|
||||
amount: 75,
|
||||
booked: false,
|
||||
excluded: false,
|
||||
invoice_collection_id: 8300,
|
||||
date: "2026-03-28T10:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: 8302,
|
||||
amount: 125,
|
||||
booked: false,
|
||||
excluded: false,
|
||||
invoice_collection_id: 8300,
|
||||
date: "2026-04-02T10:00:00.000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
Swal.fire.mockResolvedValueOnce({ isDismissed: true });
|
||||
|
||||
const wrapper = mountView();
|
||||
await nextTick();
|
||||
|
||||
await wrapper.get("[data-testid='invoicing-period-customer-invoice-1010']").trigger("click");
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
|
||||
expect(SessionUser.objects.collectedOrderInvoices.functions.split_by_month).not.toHaveBeenCalled();
|
||||
expect(invoiceQueue.addInvoiceCollectionsToQueue).not.toHaveBeenCalled();
|
||||
expect(invoiceQueue.processInvoiceCollectionQueue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("applies invoice-now loading only to the affected customer", async () => {
|
||||
sharedVariablesRef.value = {
|
||||
types: {
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
// @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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user