Files
nullcart/cms/src/components/OrderCartPanel.vue
T
nobswebdev 0cd7158f7d Align CMS title fallbacks and mutation error handling.
Add blog post untitled helper, use product title helper in order views, and surface API validation messages on product save/delete.
2026-09-16 16:20:52 +02:00

208 lines
7.7 KiB
Vue

<template>
<div>
<el-card v-if="lines.length" shadow="never">
<template #header>
<span>Order cart</span>
</template>
<div class="cms-table-scroll">
<el-table :data="lines" stripe class="order-cart-table">
<el-table-column label="Product" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
<router-link
:to="{ name: ROUTE_NAMES.ProductDetail, params: { id: row.productId } }"
class="order-line-link"
target="_blank"
rel="noopener"
>
{{ getProductTitle(row.productTitle) }}
</router-link>
</template>
</el-table-column>
<el-table-column label="Variant" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
<router-link
:to="{
name: ROUTE_NAMES.ProductVariantDetail,
params: { productId: row.productId, variantId: row.variantId }
}"
class="order-line-link"
target="_blank"
rel="noopener"
>
{{ row.variantTitle }}
</router-link>
</template>
</el-table-column>
<el-table-column prop="qty" label="Qty" width="70" />
<el-table-column label="Unit price" width="120">
<template #default="{ row }">
{{ formatFiatPrice(row.unitPriceFiat, order.fiatCurrency) }}
</template>
</el-table-column>
<el-table-column label="Line total" width="120">
<template #default="{ row }">
{{ formatFiatPrice(row.lineSubtotalFiat, order.fiatCurrency) }}
</template>
</el-table-column>
<el-table-column label="Delivery" width="150">
<template #default="{ row }">
{{ formatDeliveryMode(row.deliveryMode) }}
</template>
</el-table-column>
<el-table-column label="Fulfillment" width="140">
<template #default="{ row }">
<el-tag :type="resolveLineFulfillmentTag(row).type" size="small">
{{ resolveLineFulfillmentTag(row).label }}
</el-tag>
</template>
</el-table-column>
<el-table-column width="100" fixed="right" align="right" class-name="order-cart-action-col">
<template #default="{ row }">
<span v-if="order.failureReason" class="secondary-text">—</span>
<template v-else>
<el-button
v-if="row.deliveryMode === DeliveryMode.Auto"
link
type="primary"
title="View delivery"
@click="openAutoDelivery(row)"
>
View
</el-button>
<el-button
v-else-if="!isManualLineFulfilled(row)"
link
type="primary"
title="Mark as fulfilled"
:loading="fulfillingLineId === row.id"
@click="markAsFulfilled(row)"
>
Fulfill
</el-button>
<span v-else class="secondary-text">—</span>
</template>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
<order-line-auto-fulfillment-modal v-model="autoModalVisible" :line="selectedLine" />
</div>
</template>
<script setup lang="ts">
import { computed, ref, type PropType } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { ROUTE_NAMES } from '@/consts/routeNames';
import { useOrdersStore } from '@/stores/orders';
import { ManualLineFulfillmentStatus } from '@/types/order/ManualLineFulfillmentStatus';
import type { OrderLine } from '@/types/order/OrderLine';
import type { OrderExtended } from '@/types/order/OrderExtended';
import { DeliveryMode } from '@/types/product/DeliveryMode';
import { formatDeliveryMode } from '@/utils/product/formatDeliveryMode';
import { formatFiatPrice } from '@/utils/formatFiatPrice';
import { getProductTitle } from '@/utils/product/getProductTitle';
import { resolveAxiosErrorMessage } from '@/utils/resolveAxiosErrorMessage';
const props = defineProps({
order: {
type: Object as PropType<OrderExtended>,
required: true
}
});
const ordersStore = useOrdersStore();
const { fulfillManualLine } = ordersStore;
const selectedLine = ref<OrderLine | null>(null);
const autoModalVisible = ref(false);
const fulfillingLineId = ref<string | null>(null);
const lines = computed(() => props.order.lines ?? []);
const isManualLineFulfilled = (line: OrderLine): boolean =>
line.manualFulfillment?.status === ManualLineFulfillmentStatus.Fulfilled;
const openAutoDelivery = (line: OrderLine): void => {
selectedLine.value = line;
autoModalVisible.value = true;
};
const markAsFulfilled = async (line: OrderLine): Promise<void> => {
try {
await ElMessageBox.confirm(
`Mark "${getProductTitle(line.productTitle)}" (${line.variantTitle}) as fulfilled? This cannot be undone.`,
'Mark as fulfilled',
{
type: 'warning',
confirmButtonText: 'Mark fulfilled',
cancelButtonText: 'Cancel'
}
);
} catch {
return;
}
fulfillingLineId.value = line.id;
try {
await fulfillManualLine(props.order.id, line.id);
ElMessage.success('Line marked as fulfilled.');
} catch (error) {
const message = resolveAxiosErrorMessage(error, 'Could not mark line as fulfilled.');
ElMessage.error(message);
} finally {
fulfillingLineId.value = null;
}
};
const resolveLineFulfillmentTag = (line: OrderLine) => {
if (props.order.failureReason) {
return { label: 'Failed', type: 'danger' as const };
}
if (line.deliveryMode === DeliveryMode.Auto) {
return line.autoFulfillmentItems.length > 0
? { label: 'Auto-delivered', type: 'success' as const }
: { label: 'Pending', type: 'warning' as const };
}
if (isManualLineFulfilled(line)) {
return { label: 'Fulfilled', type: 'success' as const };
}
return { label: 'Pending', type: 'warning' as const };
};
</script>
<style scoped>
.order-line-link {
color: var(--el-color-primary);
text-decoration: none;
}
.order-line-link:hover {
text-decoration: underline;
}
.order-cart-table :deep(.order-cart-action-col .cell) {
padding-left: 8px;
padding-right: 8px;
}
</style>