Edit order pricing inline
This commit is contained in:
@@ -458,9 +458,16 @@ export type Order = {
|
||||
export type OrderItem = {
|
||||
__typename?: 'OrderItem';
|
||||
id: Scalars['ID']['output'];
|
||||
lineTotal?: Maybe<Scalars['Float']['output']>;
|
||||
productId?: Maybe<Scalars['ID']['output']>;
|
||||
productName: Scalars['String']['output'];
|
||||
quantity: Scalars['Float']['output'];
|
||||
unitPrice?: Maybe<Scalars['Float']['output']>;
|
||||
};
|
||||
|
||||
export type OrderItemPriceInput = {
|
||||
itemId: Scalars['ID']['input'];
|
||||
unitPrice: Scalars['Float']['input'];
|
||||
};
|
||||
|
||||
export enum OrderKind {
|
||||
@@ -647,8 +654,8 @@ export type RewardWithdrawalRequest = {
|
||||
export type SetOrderOfferInput = {
|
||||
deliveryFee: Scalars['Float']['input'];
|
||||
deliveryTerms: Scalars['String']['input'];
|
||||
itemPrices: Array<OrderItemPriceInput>;
|
||||
orderId: Scalars['ID']['input'];
|
||||
totalPrice: Scalars['Float']['input'];
|
||||
};
|
||||
|
||||
export type SubmitCalculationOrderInput = {
|
||||
@@ -847,7 +854,7 @@ export type ManagerOrdersQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ManagerOrdersQuery = { __typename?: 'Query', managerOrders: Array<{ __typename?: 'Order', id: string, code: string, status: OrderStatus, kind: OrderKind, customerId: string, deliveryAddress?: string | null, deliveryTerms?: string | null, deliveryFee?: number | null, totalPrice?: number | null, createdAt: any, items: Array<{ __typename?: 'OrderItem', id: string, productName: string, quantity: number }> }> };
|
||||
export type ManagerOrdersQuery = { __typename?: 'Query', managerOrders: Array<{ __typename?: 'Order', id: string, code: string, status: OrderStatus, kind: OrderKind, customerId: string, deliveryAddress?: string | null, deliveryTerms?: string | null, deliveryFee?: number | null, totalPrice?: number | null, createdAt: any, items: Array<{ __typename?: 'OrderItem', id: string, productName: string, quantity: number, unitPrice?: number | null, lineTotal?: number | null }> }> };
|
||||
|
||||
export type ManagerUsersDetailQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@@ -897,7 +904,7 @@ export type ManagerSetOrderOfferMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ManagerSetOrderOfferMutation = { __typename?: 'Mutation', managerSetOrderOffer: { __typename?: 'Order', id: string, code: string, status: OrderStatus, deliveryTerms?: string | null, totalPrice?: number | null } };
|
||||
export type ManagerSetOrderOfferMutation = { __typename?: 'Mutation', managerSetOrderOffer: { __typename?: 'Order', id: string, code: string, status: OrderStatus, deliveryTerms?: string | null, deliveryFee?: number | null, totalPrice?: number | null, items: Array<{ __typename?: 'OrderItem', id: string, unitPrice?: number | null, lineTotal?: number | null }> } };
|
||||
|
||||
export type StartOrderWorkMutationVariables = Exact<{
|
||||
orderId: Scalars['ID']['input'];
|
||||
@@ -945,7 +952,7 @@ export type MyCurrentOrdersQuery = { __typename?: 'Query', myCurrentOrders: Arra
|
||||
export type MyOrdersQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type MyOrdersQuery = { __typename?: 'Query', myOrders: Array<{ __typename?: 'Order', id: string, code: string, kind: OrderKind, status: OrderStatus, deliveryAddress?: string | null, totalPrice?: number | null, deliveryTerms?: string | null, createdAt: any, items: Array<{ __typename?: 'OrderItem', id: string, productName: string, quantity: number }> }> };
|
||||
export type MyOrdersQuery = { __typename?: 'Query', myOrders: Array<{ __typename?: 'Order', id: string, code: string, kind: OrderKind, status: OrderStatus, deliveryAddress?: string | null, totalPrice?: number | null, deliveryTerms?: string | null, deliveryFee?: number | null, createdAt: any, items: Array<{ __typename?: 'OrderItem', id: string, productName: string, quantity: number, unitPrice?: number | null, lineTotal?: number | null }> }> };
|
||||
|
||||
export type SubmitCalculationOrderMutationVariables = Exact<{
|
||||
input: SubmitCalculationOrderInput;
|
||||
@@ -1713,6 +1720,8 @@ export const ManagerOrdersDocument = gql`
|
||||
id
|
||||
productName
|
||||
quantity
|
||||
unitPrice
|
||||
lineTotal
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2011,7 +2020,13 @@ export const ManagerSetOrderOfferDocument = gql`
|
||||
code
|
||||
status
|
||||
deliveryTerms
|
||||
deliveryFee
|
||||
totalPrice
|
||||
items {
|
||||
id
|
||||
unitPrice
|
||||
lineTotal
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -2254,11 +2269,14 @@ export const MyOrdersDocument = gql`
|
||||
deliveryAddress
|
||||
totalPrice
|
||||
deliveryTerms
|
||||
deliveryFee
|
||||
createdAt
|
||||
items {
|
||||
id
|
||||
productName
|
||||
quantity
|
||||
unitPrice
|
||||
lineTotal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
export function orderLineStateText(totalPrice?: number | null) {
|
||||
return totalPrice == null
|
||||
? 'Цена уточняется менеджером'
|
||||
: 'Цена согласована менеджером';
|
||||
const PRICE_FORMATTER = new Intl.NumberFormat('ru-RU', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
export function formatPrice(value?: number | null) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${PRICE_FORMATTER.format(value)} ₽`;
|
||||
}
|
||||
|
||||
export function orderLineStateText(unitPrice?: number | null, lineTotal?: number | null) {
|
||||
if (unitPrice == null) {
|
||||
return 'Цена за единицу уточняется менеджером';
|
||||
}
|
||||
|
||||
const unitPriceLabel = formatPrice(unitPrice);
|
||||
const lineTotalLabel = formatPrice(lineTotal);
|
||||
|
||||
return lineTotalLabel
|
||||
? `Цена за единицу: ${unitPriceLabel} · Сумма позиции: ${lineTotalLabel}`
|
||||
: `Цена за единицу: ${unitPriceLabel}`;
|
||||
}
|
||||
|
||||
export function orderDeliveryStateText(deliveryTerms?: string | null) {
|
||||
return deliveryTerms?.trim() || 'Доставка уточняется менеджером';
|
||||
return deliveryTerms?.trim() || 'Условия доставки уточняются менеджером';
|
||||
}
|
||||
|
||||
export function orderLogisticsStateText(deliveryFee?: number | null) {
|
||||
const deliveryFeeLabel = formatPrice(deliveryFee);
|
||||
return deliveryFeeLabel || 'Стоимость логистики уточняется менеджером';
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user