Edit order pricing inline

This commit is contained in:
Ruslan Bakiev
2026-04-04 11:16:16 +07:00
parent 7dc0f59ffb
commit 8c5e95b730
8 changed files with 245 additions and 18 deletions

View File

@@ -5,9 +5,13 @@ import {
CompleteOrderDocument,
ManagerFinalizeOrderDocument,
ManagerOrdersDocument,
ManagerSetOrderOfferDocument,
StartOrderWorkDocument,
type ManagerOrdersQuery,
} from '~/composables/graphql/generated';
import {
formatPrice,
orderLogisticsStateText,
orderDeliveryStateText,
orderLineStateText,
} from '~/composables/useOrderDetailPresentation';
@@ -19,21 +23,125 @@ definePageMeta({
const route = useRoute();
const orderId = computed(() => String(route.params.id || ''));
type ManagerOrderItem = ManagerOrdersQuery['managerOrders'][number];
const ordersQuery = useQuery(ManagerOrdersDocument, { status: null });
const finalizeMutation = useMutation(ManagerFinalizeOrderDocument);
const blockMutation = useMutation(BlockOrderDocument);
const startWorkMutation = useMutation(StartOrderWorkDocument);
const completeWorkMutation = useMutation(CompleteOrderDocument);
const setOfferMutation = useMutation(ManagerSetOrderOfferDocument);
const currentOrder = computed(() =>
(ordersQuery.result.value?.managerOrders ?? []).find((item) => item.id === orderId.value),
const itemPriceDrafts = reactive<Record<string, string>>({});
const deliveryTermsDraft = ref('');
const deliveryFeeDraft = ref('');
const currentOrder = computed<ManagerOrderItem | null>(() =>
(ordersQuery.result.value?.managerOrders ?? []).find((item: ManagerOrderItem) => item.id === orderId.value) ?? null,
);
watch(
currentOrder,
(order) => {
for (const key of Object.keys(itemPriceDrafts)) {
delete itemPriceDrafts[key];
}
if (!order) {
deliveryTermsDraft.value = '';
deliveryFeeDraft.value = '';
return;
}
for (const item of order.items) {
itemPriceDrafts[item.id] = item.unitPrice == null ? '' : String(item.unitPrice);
}
deliveryTermsDraft.value = order.deliveryTerms ?? '';
deliveryFeeDraft.value = order.deliveryFee == null ? '' : String(order.deliveryFee);
},
{ immediate: true },
);
function parseMoneyDraft(value: string) {
const trimmed = String(value).replace(',', '.').trim();
if (!trimmed) {
return null;
}
const normalized = Number(trimmed);
if (!Number.isFinite(normalized) || normalized < 0) {
return null;
}
return Math.round((normalized + Number.EPSILON) * 100) / 100;
}
function draftUnitPrice(itemId: string, fallback?: number | null) {
return parseMoneyDraft(itemPriceDrafts[itemId] ?? '') ?? fallback ?? null;
}
function draftLineTotal(item: ManagerOrderItem['items'][number]) {
const unitPrice = draftUnitPrice(item.id, item.unitPrice);
if (unitPrice == null) {
return null;
}
return Math.round((unitPrice * item.quantity + Number.EPSILON) * 100) / 100;
}
const draftDeliveryTerms = computed(() => deliveryTermsDraft.value.trim() || currentOrder.value?.deliveryTerms || null);
const draftDeliveryFee = computed(() => parseMoneyDraft(deliveryFeeDraft.value) ?? currentOrder.value?.deliveryFee ?? null);
const offerReady = computed(() => {
if (!currentOrder.value) {
return false;
}
const deliveryFee = parseMoneyDraft(deliveryFeeDraft.value);
if (deliveryFee == null) {
return false;
}
return currentOrder.value.items.every((item) => parseMoneyDraft(itemPriceDrafts[item.id] ?? '') != null);
});
const offerTotal = computed(() => {
if (!currentOrder.value || !offerReady.value) {
return null;
}
const productsTotal = currentOrder.value.items.reduce((sum, item) => (
sum + item.quantity * (parseMoneyDraft(itemPriceDrafts[item.id] ?? '') ?? 0)
), 0);
return Math.round((productsTotal + (parseMoneyDraft(deliveryFeeDraft.value) ?? 0) + Number.EPSILON) * 100) / 100;
});
async function refetchOrder() {
await ordersQuery.refetch({ status: null });
}
async function saveOffer() {
if (!currentOrder.value || !offerReady.value) {
return;
}
await setOfferMutation.mutate({
input: {
orderId: currentOrder.value.id,
itemPrices: currentOrder.value.items.map((item) => ({
itemId: item.id,
unitPrice: parseMoneyDraft(itemPriceDrafts[item.id] ?? '') ?? 0,
})),
deliveryTerms: deliveryTermsDraft.value.trim(),
deliveryFee: parseMoneyDraft(deliveryFeeDraft.value) ?? 0,
},
});
await refetchOrder();
}
async function approveOrder() {
if (!currentOrder.value) {
return;
@@ -112,9 +220,23 @@ async function completeOrder() {
<div class="surface-card rounded-3xl p-5">
<h2 class="text-xl font-bold text-[#123824]">Состав заказа</h2>
<ul class="mt-4 space-y-3">
<li v-for="item in currentOrder.items" :key="item.id" class="manager-mini-card space-y-2">
<p class="text-sm font-semibold text-[#123824]">{{ item.productName }} × {{ item.quantity }}</p>
<p class="text-sm text-[#5c7b69]">{{ orderLineStateText(currentOrder.totalPrice) }}</p>
<li v-for="item in currentOrder.items" :key="item.id" class="manager-mini-card space-y-3">
<div class="space-y-1">
<p class="text-sm font-semibold text-[#123824]">{{ item.productName }} × {{ item.quantity }}</p>
<p class="text-sm text-[#5c7b69]">{{ orderLineStateText(draftUnitPrice(item.id, item.unitPrice), draftLineTotal(item)) }}</p>
</div>
<label class="form-control">
<span class="mb-2 text-xs font-semibold uppercase tracking-[0.22em] text-[#6a8a76]">Цена за единицу</span>
<input
v-model="itemPriceDrafts[item.id]"
type="number"
min="0"
step="0.01"
placeholder="Например, 125.50"
class="input input-bordered w-full rounded-2xl bg-white"
>
</label>
</li>
</ul>
</div>
@@ -125,9 +247,49 @@ async function completeOrder() {
<div class="manager-mini-card text-sm text-[#123824]">
Адрес: {{ currentOrder.deliveryAddress || 'Адрес пока не указан' }}
</div>
<div class="manager-mini-card text-sm text-[#123824]">
{{ orderDeliveryStateText(currentOrder.deliveryTerms) }}
<div class="manager-mini-card space-y-3 text-sm text-[#123824]">
<p>{{ orderDeliveryStateText(draftDeliveryTerms) }}</p>
<label class="form-control">
<span class="mb-2 text-xs font-semibold uppercase tracking-[0.22em] text-[#6a8a76]">Комментарий по доставке</span>
<input
v-model="deliveryTermsDraft"
type="text"
placeholder="Например, доставка до склада 2-3 дня"
class="input input-bordered w-full rounded-2xl bg-white"
>
</label>
</div>
<div class="manager-mini-card space-y-3 text-sm text-[#123824]">
<p>{{ orderLogisticsStateText(draftDeliveryFee) }}</p>
<label class="form-control">
<span class="mb-2 text-xs font-semibold uppercase tracking-[0.22em] text-[#6a8a76]">Стоимость логистики</span>
<input
v-model="deliveryFeeDraft"
type="number"
min="0"
step="0.01"
placeholder="Например, 3000"
class="input input-bordered w-full rounded-2xl bg-white"
>
</label>
</div>
</div>
<div class="mt-4 flex flex-wrap items-center justify-between gap-3 border-t border-[#d6ebde] pt-4">
<p class="text-sm text-[#5c7b69]">
{{
offerTotal == null
? 'Итог по заказу посчитается автоматически после заполнения всех цен.'
: `Предварительный итог: ${formatPrice(offerTotal)}`
}}
</p>
<button
class="btn btn-primary rounded-full border-0 px-5"
:disabled="!offerReady || setOfferMutation.loading.value"
@click="saveOffer"
>
Сохранить условия
</button>
</div>
</div>

View File

@@ -5,6 +5,7 @@ import {
type MyOrdersQuery,
} from '~/composables/graphql/generated';
import {
orderLogisticsStateText,
orderDeliveryStateText,
orderLineStateText,
} from '~/composables/useOrderDetailPresentation';
@@ -53,7 +54,7 @@ const currentOrder = computed<OrderItem | null>(() =>
class="manager-mini-card space-y-2"
>
<p class="text-sm font-semibold text-[#123824]">{{ item.productName }} × {{ item.quantity }}</p>
<p class="text-sm text-[#5c7b69]">{{ orderLineStateText(currentOrder.totalPrice) }}</p>
<p class="text-sm text-[#5c7b69]">{{ orderLineStateText(item.unitPrice, item.lineTotal) }}</p>
</li>
</ul>
</div>
@@ -67,6 +68,9 @@ const currentOrder = computed<OrderItem | null>(() =>
<div class="manager-mini-card text-sm text-[#123824]">
{{ orderDeliveryStateText(currentOrder.deliveryTerms) }}
</div>
<div class="manager-mini-card text-sm text-[#123824]">
{{ orderLogisticsStateText(currentOrder.deliveryFee) }}
</div>
</div>
</div>
</div>