Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06e1552a47 |
@@ -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')">
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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'}",
|
||||||
|
|||||||
@@ -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'}",
|
||||||
|
|||||||
@@ -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'}",
|
||||||
|
|||||||
@@ -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'}",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user