- Added `PosOrderRegistrationField.vue` for managing inline registration field inputs with autosave behavior. - Introduced `useOrderMetadataAutosave.js` composable for seamless value normalization, dirty tracking, and delayed persistence. - Refactored mobile and desktop POS components to integrate `useOrderMetadataAutosave` for improved field management (e.g., `POSOrderReference.vue`, `PosOrderLicensePlates.vue`). - Updated e2e tests to cover registration field scenarios, including input normalization and persistence (`pos-mobile-order-flow.spec.js`, `admin-pos-orders.spec.ts`). - Enhanced styling and responsiveness for registration fields in `pos.css`.
117 lines
3.1 KiB
Vue
117 lines
3.1 KiB
Vue
<script setup>
|
|
import { computed, defineProps, nextTick, ref, watch } from "vue";
|
|
import { useI18n } from "vue-i18n";
|
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
|
import { useOrderMetadataAutosave } from "@/composables/useOrderMetadataAutosave.js";
|
|
|
|
const props = defineProps({
|
|
index: {
|
|
type: Number,
|
|
required: true,
|
|
},
|
|
order_id: {
|
|
type: [Number, String],
|
|
required: true,
|
|
},
|
|
modelValue: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
loadOrder: {
|
|
type: Function,
|
|
required: true,
|
|
},
|
|
});
|
|
|
|
const { t } = useI18n();
|
|
|
|
const normalizeRegistrationDraft = (value) => String(value ?? "").toUpperCase();
|
|
|
|
const inputId = `pos-order-registration-input-${props.index}`;
|
|
const isEditing = ref(false);
|
|
const fieldKey = computed(() => `reg_${props.index}`);
|
|
let autosave;
|
|
|
|
autosave = useOrderMetadataAutosave({
|
|
source: () => props.modelValue,
|
|
normalizeValue: normalizeRegistrationDraft,
|
|
saveValue: async (value) => {
|
|
await SessionUser.objects.orders.set[fieldKey.value](props.order_id, value);
|
|
return value;
|
|
},
|
|
onSaved: async () => {
|
|
await props.loadOrder();
|
|
await nextTick();
|
|
autosave.syncFromSource(props.modelValue);
|
|
},
|
|
});
|
|
|
|
const hasValue = computed(() => autosave.draft.value.length > 0);
|
|
const buttonTestId = computed(() => (hasValue.value ? `pos-order-registration-${props.index}` : `pos-order-registration-add-${props.index}`));
|
|
|
|
watch(() => autosave.draft.value, () => {
|
|
if (isEditing.value) {
|
|
autosave.scheduleSave();
|
|
}
|
|
});
|
|
|
|
const focusInput = () => {
|
|
document.getElementById(inputId)?.focus();
|
|
};
|
|
|
|
const openEditor = () => {
|
|
if (isEditing.value) {
|
|
return;
|
|
}
|
|
|
|
isEditing.value = true;
|
|
nextTick(() => {
|
|
focusInput();
|
|
});
|
|
};
|
|
|
|
const closeEditor = async () => {
|
|
isEditing.value = false;
|
|
await autosave.flush();
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="control pos-registration-control" :class="{ 'is-loading': autosave.isSaving.value }">
|
|
<div v-if="isEditing" class="pos-registration-field pos-registration-field--editing">
|
|
<label class="pos-registration-field__label" :for="inputId">{{ t('pos.order.reg') }} {{ props.index }}</label>
|
|
<input
|
|
:id="inputId"
|
|
v-model="autosave.draft.value"
|
|
:data-testid="`pos-order-registration-input-${props.index}`"
|
|
class="pos-registration-field__input"
|
|
type="text"
|
|
autocomplete="off"
|
|
@blur="closeEditor()"
|
|
@keydown.enter.prevent="closeEditor()"
|
|
/>
|
|
</div>
|
|
<button
|
|
v-else
|
|
class="pos-registration-field"
|
|
:class="{ 'pos-registration-field--add': !hasValue }"
|
|
type="button"
|
|
:data-testid="buttonTestId"
|
|
:aria-label="!hasValue ? `${t('common.add')} ${t('pos.order.reg')} ${props.index}` : undefined"
|
|
@click="openEditor()"
|
|
>
|
|
<span class="pos-registration-field__label">{{ t('pos.order.reg') }} {{ props.index }}</span>
|
|
<span
|
|
class="pos-registration-field__value"
|
|
:class="{ 'pos-registration-field__value--muted': !hasValue }"
|
|
>
|
|
{{ hasValue ? autosave.draft.value : `+ ${t('common.add')}` }}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
|
|
</style>
|