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

@@ -458,9 +458,16 @@ export type Order = {
export type OrderItem = { export type OrderItem = {
__typename?: 'OrderItem'; __typename?: 'OrderItem';
id: Scalars['ID']['output']; id: Scalars['ID']['output'];
lineTotal?: Maybe<Scalars['Float']['output']>;
productId?: Maybe<Scalars['ID']['output']>; productId?: Maybe<Scalars['ID']['output']>;
productName: Scalars['String']['output']; productName: Scalars['String']['output'];
quantity: Scalars['Float']['output']; quantity: Scalars['Float']['output'];
unitPrice?: Maybe<Scalars['Float']['output']>;
};
export type OrderItemPriceInput = {
itemId: Scalars['ID']['input'];
unitPrice: Scalars['Float']['input'];
}; };
export enum OrderKind { export enum OrderKind {
@@ -647,8 +654,8 @@ export type RewardWithdrawalRequest = {
export type SetOrderOfferInput = { export type SetOrderOfferInput = {
deliveryFee: Scalars['Float']['input']; deliveryFee: Scalars['Float']['input'];
deliveryTerms: Scalars['String']['input']; deliveryTerms: Scalars['String']['input'];
itemPrices: Array<OrderItemPriceInput>;
orderId: Scalars['ID']['input']; orderId: Scalars['ID']['input'];
totalPrice: Scalars['Float']['input'];
}; };
export type SubmitCalculationOrderInput = { 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; }>; 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<{ export type StartOrderWorkMutationVariables = Exact<{
orderId: Scalars['ID']['input']; orderId: Scalars['ID']['input'];
@@ -945,7 +952,7 @@ export type MyCurrentOrdersQuery = { __typename?: 'Query', myCurrentOrders: Arra
export type MyOrdersQueryVariables = Exact<{ [key: string]: never; }>; 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<{ export type SubmitCalculationOrderMutationVariables = Exact<{
input: SubmitCalculationOrderInput; input: SubmitCalculationOrderInput;
@@ -1713,6 +1720,8 @@ export const ManagerOrdersDocument = gql`
id id
productName productName
quantity quantity
unitPrice
lineTotal
} }
} }
} }
@@ -2011,7 +2020,13 @@ export const ManagerSetOrderOfferDocument = gql`
code code
status status
deliveryTerms deliveryTerms
deliveryFee
totalPrice totalPrice
items {
id
unitPrice
lineTotal
}
} }
} }
`; `;
@@ -2254,11 +2269,14 @@ export const MyOrdersDocument = gql`
deliveryAddress deliveryAddress
totalPrice totalPrice
deliveryTerms deliveryTerms
deliveryFee
createdAt createdAt
items { items {
id id
productName productName
quantity quantity
unitPrice
lineTotal
} }
} }
} }

View File

@@ -1,9 +1,34 @@
export function orderLineStateText(totalPrice?: number | null) { const PRICE_FORMATTER = new Intl.NumberFormat('ru-RU', {
return totalPrice == null 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) { export function orderDeliveryStateText(deliveryTerms?: string | null) {
return deliveryTerms?.trim() || 'Доставка уточняется менеджером'; return deliveryTerms?.trim() || 'Условия доставки уточняются менеджером';
}
export function orderLogisticsStateText(deliveryFee?: number | null) {
const deliveryFeeLabel = formatPrice(deliveryFee);
return deliveryFeeLabel || 'Стоимость логистики уточняется менеджером';
} }

View File

@@ -5,9 +5,13 @@ import {
CompleteOrderDocument, CompleteOrderDocument,
ManagerFinalizeOrderDocument, ManagerFinalizeOrderDocument,
ManagerOrdersDocument, ManagerOrdersDocument,
ManagerSetOrderOfferDocument,
StartOrderWorkDocument, StartOrderWorkDocument,
type ManagerOrdersQuery,
} from '~/composables/graphql/generated'; } from '~/composables/graphql/generated';
import { import {
formatPrice,
orderLogisticsStateText,
orderDeliveryStateText, orderDeliveryStateText,
orderLineStateText, orderLineStateText,
} from '~/composables/useOrderDetailPresentation'; } from '~/composables/useOrderDetailPresentation';
@@ -19,21 +23,125 @@ definePageMeta({
const route = useRoute(); const route = useRoute();
const orderId = computed(() => String(route.params.id || '')); const orderId = computed(() => String(route.params.id || ''));
type ManagerOrderItem = ManagerOrdersQuery['managerOrders'][number];
const ordersQuery = useQuery(ManagerOrdersDocument, { status: null }); const ordersQuery = useQuery(ManagerOrdersDocument, { status: null });
const finalizeMutation = useMutation(ManagerFinalizeOrderDocument); const finalizeMutation = useMutation(ManagerFinalizeOrderDocument);
const blockMutation = useMutation(BlockOrderDocument); const blockMutation = useMutation(BlockOrderDocument);
const startWorkMutation = useMutation(StartOrderWorkDocument); const startWorkMutation = useMutation(StartOrderWorkDocument);
const completeWorkMutation = useMutation(CompleteOrderDocument); const completeWorkMutation = useMutation(CompleteOrderDocument);
const setOfferMutation = useMutation(ManagerSetOrderOfferDocument);
const currentOrder = computed(() => const itemPriceDrafts = reactive<Record<string, string>>({});
(ordersQuery.result.value?.managerOrders ?? []).find((item) => item.id === orderId.value), 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() { async function refetchOrder() {
await ordersQuery.refetch({ status: null }); 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() { async function approveOrder() {
if (!currentOrder.value) { if (!currentOrder.value) {
return; return;
@@ -112,9 +220,23 @@ async function completeOrder() {
<div class="surface-card rounded-3xl p-5"> <div class="surface-card rounded-3xl p-5">
<h2 class="text-xl font-bold text-[#123824]">Состав заказа</h2> <h2 class="text-xl font-bold text-[#123824]">Состав заказа</h2>
<ul class="mt-4 space-y-3"> <ul class="mt-4 space-y-3">
<li v-for="item in currentOrder.items" :key="item.id" class="manager-mini-card space-y-2"> <li v-for="item in currentOrder.items" :key="item.id" class="manager-mini-card space-y-3">
<p class="text-sm font-semibold text-[#123824]">{{ item.productName }} × {{ item.quantity }}</p> <div class="space-y-1">
<p class="text-sm text-[#5c7b69]">{{ orderLineStateText(currentOrder.totalPrice) }}</p> <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> </li>
</ul> </ul>
</div> </div>
@@ -125,9 +247,49 @@ async function completeOrder() {
<div class="manager-mini-card text-sm text-[#123824]"> <div class="manager-mini-card text-sm text-[#123824]">
Адрес: {{ currentOrder.deliveryAddress || 'Адрес пока не указан' }} Адрес: {{ currentOrder.deliveryAddress || 'Адрес пока не указан' }}
</div> </div>
<div class="manager-mini-card text-sm text-[#123824]"> <div class="manager-mini-card space-y-3 text-sm text-[#123824]">
{{ orderDeliveryStateText(currentOrder.deliveryTerms) }} <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>
<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>
</div> </div>

View File

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

View File

@@ -14,6 +14,8 @@ query ManagerOrders($status: OrderStatus, $customerId: ID) {
id id
productName productName
quantity quantity
unitPrice
lineTotal
} }
} }
} }

View File

@@ -4,6 +4,12 @@ mutation ManagerSetOrderOffer($input: SetOrderOfferInput!) {
code code
status status
deliveryTerms deliveryTerms
deliveryFee
totalPrice totalPrice
items {
id
unitPrice
lineTotal
}
} }
} }

View File

@@ -7,11 +7,14 @@ query MyOrders {
deliveryAddress deliveryAddress
totalPrice totalPrice
deliveryTerms deliveryTerms
deliveryFee
createdAt createdAt
items { items {
id id
productName productName
quantity quantity
unitPrice
lineTotal
} }
} }
} }

View File

@@ -232,6 +232,8 @@ type OrderItem {
productId: ID productId: ID
productName: String! productName: String!
quantity: Float! quantity: Float!
unitPrice: Float
lineTotal: Float
} }
type OrderStatusEvent { type OrderStatusEvent {
@@ -428,9 +430,14 @@ input SubmitCalculationOrderInput {
input SetOrderOfferInput { input SetOrderOfferInput {
orderId: ID! orderId: ID!
itemPrices: [OrderItemPriceInput!]!
deliveryTerms: String! deliveryTerms: String!
deliveryFee: Float! deliveryFee: Float!
totalPrice: Float! }
input OrderItemPriceInput {
itemId: ID!
unitPrice: Float!
} }
input BlockOrderInput { input BlockOrderInput {