Files
pleno-vue/src/components/displays/department/pos/order/PosDesktopOrderWorkspace.vue
T

309 lines
8.4 KiB
Vue

<script setup>
import { computed, onMounted, ref, useSlots, watch } from "vue";
import WhiteBox from "@/components/displays/boxes/WhiteBox.vue";
import PosOrderItemsCurrent from "@/components/displays/department/pos/PosOrderItemsCurrent.vue";
import PosSelectedCustomer from "@/components/displays/department/pos/PosSelectedCustomer.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { getDepartmentName, getDepartments } from "@/components/pagination/departmentTabs.vue";
const props = defineProps({
mode: {
type: String,
default: "summary",
validator: (value) => ["summary", "catalog"].includes(value),
},
rootTestId: {
type: String,
default: null,
},
orderId: {
type: [Number, String],
default: null,
},
orderItems: {
type: Array,
default: () => [],
},
loadOrderItems: {
type: Function,
default: null,
},
editPossible: {
type: Boolean,
default: false,
},
showCatalogPane: {
type: Boolean,
default: false,
},
showCustomerRail: {
type: Boolean,
default: true,
},
summaryOrder: {
type: Object,
default: null,
},
});
const slots = useSlots();
const fetchedSummaryOrder = ref(null);
const departmentTabsLoaded = ref(false);
const parsePositiveInteger = (value) => {
const parsed = Number.parseInt(value, 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
};
const normalizedOrderId = computed(() => parsePositiveInteger(props.orderId));
const resolvedSummaryOrder = computed(() => props.summaryOrder || fetchedSummaryOrder.value || null);
const shouldShowRail = computed(() => props.showCustomerRail && !props.showCatalogPane);
const shouldShowInlineSummary = computed(() => !props.showCatalogPane);
const resolvedDepartmentLabel = computed(() => {
const departmentId = parsePositiveInteger(resolvedSummaryOrder.value?.department_id);
if (!departmentId) {
return "";
}
return getDepartmentName(departmentId) || String(departmentId);
});
const resolvedCreatedAt = computed(() => {
return resolvedSummaryOrder.value?.created_at || "";
});
const hasInlineSummaryTags = computed(() => {
return shouldShowInlineSummary.value && Boolean(resolvedDepartmentLabel.value || resolvedCreatedAt.value);
});
const loadDepartmentTabs = async () => {
if (departmentTabsLoaded.value) {
return;
}
try {
await getDepartments();
} catch (error) {
console.error("Unable to load POS department labels", error);
} finally {
departmentTabsLoaded.value = true;
}
};
const loadSummaryOrder = async (candidateOrderId = normalizedOrderId.value) => {
if (props.summaryOrder) {
fetchedSummaryOrder.value = null;
return;
}
if (!candidateOrderId) {
fetchedSummaryOrder.value = null;
return;
}
try {
fetchedSummaryOrder.value = await SessionUser.objects.orders.get.single(candidateOrderId);
} catch (error) {
console.error("Unable to load POS order summary", error);
fetchedSummaryOrder.value = null;
}
};
onMounted(async () => {
await loadDepartmentTabs();
await loadSummaryOrder();
});
watch(() => props.summaryOrder, async () => {
await loadSummaryOrder();
}, { deep: true });
watch(normalizedOrderId, async (nextOrderId, previousOrderId) => {
if (nextOrderId === previousOrderId) {
return;
}
await loadSummaryOrder(nextOrderId);
});
</script>
<template>
<div
class="pos-shell pos-order-detail"
:class="{ 'pos-order-detail--editing': showCatalogPane }"
:data-testid="rootTestId || undefined"
>
<div class="pos-main pos-order-detail__main" data-testid="pos-order-main">
<div v-if="$slots.leading" class="pos-order-detail__leading">
<slot name="leading"></slot>
</div>
<div
class="pos-order-detail__workspace"
data-testid="pos-order-workspace"
:class="{ 'pos-order-detail__workspace--editing': showCatalogPane }"
>
<div
v-if="showCatalogPane"
class="pos-order-detail__products"
data-testid="pos-order-add-items-panel"
>
<slot name="catalog"></slot>
</div>
<div
class="pos-order-detail__order-column"
:class="{ 'pos-order-detail__order-column--editing': showCatalogPane }"
>
<WhiteBox class="pos-card pos-card--flush pos-order-detail__order" data-testid="pos-order-card">
<PosOrderItemsCurrent
:orderItems="orderItems"
:orderId="orderId"
:loadOrderItems="loadOrderItems"
:editPossible="editPossible"
:stackMetadata="showCatalogPane"
variant="order-detail"
>
<template #default>
<div v-if="$slots.status" class="is-hidden-touch pos-order-detail__status-group">
<slot name="status"></slot>
</div>
</template>
<template #headingcenter>
<div class="pos-inline-tags is-hidden-touch" v-if="hasInlineSummaryTags">
<span
v-if="resolvedDepartmentLabel"
class="pos-inline-tag"
data-testid="pos-order-inline-department"
>
<span class="pos-inline-tag__label">{{ $t('pos.order.department') }}:</span>
<span class="pos-inline-tag__value">{{ resolvedDepartmentLabel }}</span>
</span>
<span
v-if="resolvedCreatedAt"
class="pos-inline-tag"
data-testid="pos-order-inline-created"
>
<span class="pos-inline-tag__label">{{ $t('common.created') }}:</span>
<span class="pos-inline-tag__value">{{ resolvedCreatedAt }}</span>
</span>
</div>
<slot name="headingcenter"></slot>
</template>
<template v-if="$slots.headerActions" #headerActions>
<slot name="headerActions"></slot>
</template>
<template v-if="$slots.beforeItems" #beforeItems>
<slot name="beforeItems"></slot>
</template>
<template v-if="$slots.attachments" #attachments>
<slot name="attachments"></slot>
</template>
<template v-if="$slots.other" #other>
<slot name="other"></slot>
</template>
<template v-if="$slots.details" #details>
<slot name="details"></slot>
</template>
<template #bottom-order-items>
<slot name="bottom-order-items"></slot>
</template>
</PosOrderItemsCurrent>
</WhiteBox>
<div
v-if="$slots.orderFooter"
class="pos-order-detail__order-footer"
>
<slot name="orderFooter"></slot>
</div>
</div>
</div>
</div>
<div v-if="shouldShowRail" class="pos-rail pos-rail--sticky" data-testid="pos-order-rail">
<WhiteBox class="pos-card pos-card--flush is-hidden-touch" data-testid="pos-customer-card">
<PosSelectedCustomer variant="order-detail" />
</WhiteBox>
<div v-if="$slots['rail-actions']" class="pos-order-detail__rail-actions">
<slot name="rail-actions"></slot>
</div>
</div>
</div>
</template>
<style scoped>
.pos-order-detail__main {
display: flex;
flex-direction: column;
gap: 1rem;
}
.pos-order-detail__leading,
.pos-order-detail__rail-actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.pos-order-detail__workspace {
display: grid;
gap: 1rem;
align-items: start;
}
.pos-order-detail__order {
min-width: 0;
width: 100%;
margin-bottom: 0;
}
.pos-order-detail__order-column {
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
align-self: start;
}
.pos-order-detail__products {
min-width: 0;
width: 100%;
}
.pos-order-detail__order-footer {
min-width: 0;
}
.pos-order-detail--editing {
grid-template-columns: minmax(0, 1fr);
}
.pos-order-detail__workspace--editing {
grid-template-columns: minmax(0, 1fr) minmax(20rem, 23.75rem);
}
.pos-order-detail__order-column--editing {
grid-column: 2;
}
@media screen and (max-width: 1023px) {
.pos-order-detail__workspace--editing {
grid-template-columns: 1fr;
}
.pos-order-detail__order-column--editing {
grid-column: auto;
}
}
</style>