Simplify manager order editing UI
This commit is contained in:
@@ -1,12 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useMutation, useQuery } from '@vue/apollo-composable';
|
||||
import {
|
||||
CompleteOrderDocument,
|
||||
Decision,
|
||||
ManagerFinalizeOrderDocument,
|
||||
ManagerSetOrderOfferDocument,
|
||||
ManagerSetOrderStatusDocument,
|
||||
OrderStatus,
|
||||
OrderDetailDocument,
|
||||
StartOrderWorkDocument,
|
||||
type OrderDetailQuery,
|
||||
} from '~/composables/graphql/generated';
|
||||
import {
|
||||
@@ -24,9 +22,8 @@ const route = useRoute();
|
||||
const orderId = computed(() => String(route.params.id || ''));
|
||||
|
||||
type ManagerOrderItem = NonNullable<OrderDetailQuery['order']>;
|
||||
type StatusAction = 'APPROVE' | 'REJECT' | 'START' | 'COMPLETE';
|
||||
type StatusActionOption = {
|
||||
value: StatusAction;
|
||||
type StatusOption = {
|
||||
value: OrderStatus;
|
||||
label: string;
|
||||
};
|
||||
|
||||
@@ -34,10 +31,8 @@ const orderQuery = useQuery(OrderDetailDocument, () => ({
|
||||
id: orderId.value,
|
||||
}));
|
||||
|
||||
const completeOrderMutation = useMutation(CompleteOrderDocument);
|
||||
const finalizeOrderMutation = useMutation(ManagerFinalizeOrderDocument);
|
||||
const startWorkMutation = useMutation(StartOrderWorkDocument);
|
||||
const setOfferMutation = useMutation(ManagerSetOrderOfferDocument);
|
||||
const setOrderStatusMutation = useMutation(ManagerSetOrderStatusDocument);
|
||||
|
||||
const itemPriceDrafts = reactive<Record<string, string>>({});
|
||||
const deliveryTermsDraft = ref('');
|
||||
@@ -46,7 +41,7 @@ const editingPriceItemId = ref<string | null>(null);
|
||||
const editingDeliveryTerms = ref(false);
|
||||
const editingDeliveryFee = ref(false);
|
||||
const editingStatus = ref(false);
|
||||
const statusActionDraft = ref<StatusAction | ''>('');
|
||||
const statusDraft = ref<OrderStatus | ''>('');
|
||||
const autosavePending = ref(false);
|
||||
const statusMutationPending = ref(false);
|
||||
let autosaveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -64,7 +59,7 @@ watch(
|
||||
editingDeliveryTerms.value = false;
|
||||
editingDeliveryFee.value = false;
|
||||
editingStatus.value = false;
|
||||
statusActionDraft.value = '';
|
||||
statusDraft.value = '';
|
||||
|
||||
for (const key of Object.keys(itemPriceDrafts)) {
|
||||
delete itemPriceDrafts[key];
|
||||
@@ -101,63 +96,22 @@ function parseMoneyDraft(value: string) {
|
||||
}
|
||||
|
||||
const draftDeliveryTerms = computed(() => deliveryTermsDraft.value.trim() || currentOrder.value?.deliveryTerms || null);
|
||||
const draftDeliveryFee = computed(() => parseMoneyDraft(deliveryFeeDraft.value) ?? currentOrder.value?.deliveryFee ?? null);
|
||||
const canEditOffer = computed(() => (
|
||||
currentOrder.value != null
|
||||
&& ['NEW', 'MANAGER_PROCESSING', 'WAITING_DOUBLE_CONFIRM', 'CONFIRMED'].includes(currentOrder.value.status)
|
||||
));
|
||||
const draftDeliveryFee = computed(() => parseMoneyDraft(deliveryFeeDraft.value));
|
||||
const canEditOffer = computed(() => currentOrder.value != null);
|
||||
const activePriceItemIds = computed(() => (
|
||||
editingPriceItemId.value ? [editingPriceItemId.value] : []
|
||||
));
|
||||
const managerStatusSummary = computed(() => {
|
||||
if (!currentOrder.value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (['NEW', 'MANAGER_PROCESSING'].includes(currentOrder.value.status)) {
|
||||
return 'Заполните цены по позициям и логистике. После этого заказ можно перевести в предложение.';
|
||||
}
|
||||
|
||||
if (['WAITING_DOUBLE_CONFIRM', 'CONFIRMED'].includes(currentOrder.value.status)) {
|
||||
return 'Предложение уже собрано. Следующий шаг для менеджера: перевести заказ в работу или остановить его.';
|
||||
}
|
||||
|
||||
if (currentOrder.value.status === 'IN_PROGRESS') {
|
||||
return 'Заказ уже в работе. После исполнения его можно завершить.';
|
||||
}
|
||||
|
||||
if (currentOrder.value.status === 'COMPLETED') {
|
||||
return 'Заказ завершён и больше не редактируется.';
|
||||
}
|
||||
|
||||
return 'Заказ остановлен. Если нужно продолжить работу, сначала поправьте условия и затем снова переведите его в предложение.';
|
||||
});
|
||||
const statusActions = computed<StatusActionOption[]>(() => {
|
||||
if (!currentOrder.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const options: StatusActionOption[] = [];
|
||||
|
||||
if (offerReady.value && ['NEW', 'MANAGER_PROCESSING', 'MANAGER_BLOCKED', 'CLIENT_REJECTED', 'MANAGER_REJECTED'].includes(currentOrder.value.status)) {
|
||||
options.push({ value: 'APPROVE', label: 'Предложение' });
|
||||
}
|
||||
|
||||
if (['NEW', 'MANAGER_PROCESSING', 'WAITING_DOUBLE_CONFIRM', 'CONFIRMED', 'MANAGER_BLOCKED'].includes(currentOrder.value.status)) {
|
||||
options.push({ value: 'REJECT', label: 'Отклонен' });
|
||||
}
|
||||
|
||||
if (['WAITING_DOUBLE_CONFIRM', 'CONFIRMED'].includes(currentOrder.value.status)) {
|
||||
options.push({ value: 'START', label: 'В работе' });
|
||||
}
|
||||
|
||||
if (currentOrder.value.status === 'IN_PROGRESS') {
|
||||
options.push({ value: 'COMPLETE', label: 'Завершен' });
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
||||
const canEditStatus = computed(() => statusActions.value.length > 0);
|
||||
const statusOptions: StatusOption[] = [
|
||||
{ value: OrderStatus.New, label: 'Заявка' },
|
||||
{ value: OrderStatus.ManagerProcessing, label: 'В обработке' },
|
||||
{ value: OrderStatus.WaitingDoubleConfirm, label: 'Предложение' },
|
||||
{ value: OrderStatus.Confirmed, label: 'Подтвержден' },
|
||||
{ value: OrderStatus.InProgress, label: 'В работе' },
|
||||
{ value: OrderStatus.Completed, label: 'Завершен' },
|
||||
{ value: OrderStatus.ManagerBlocked, label: 'Пауза' },
|
||||
{ value: OrderStatus.ManagerRejected, label: 'Отклонен менеджером' },
|
||||
{ value: OrderStatus.ClientRejected, label: 'Отклонен клиентом' },
|
||||
];
|
||||
|
||||
const offerSignature = computed(() => {
|
||||
if (!currentOrder.value) {
|
||||
@@ -224,7 +178,7 @@ async function focusElement(selector: string) {
|
||||
}
|
||||
|
||||
async function saveOffer() {
|
||||
if (!currentOrder.value || !offerReady.value || !canEditOffer.value) {
|
||||
if (!currentOrder.value || !canEditOffer.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -235,10 +189,10 @@ async function saveOffer() {
|
||||
orderId: currentOrder.value.id,
|
||||
itemPrices: currentOrder.value.items.map((item) => ({
|
||||
itemId: item.id,
|
||||
unitPrice: parseMoneyDraft(itemPriceDrafts[item.id] ?? '') ?? 0,
|
||||
unitPrice: parseMoneyDraft(itemPriceDrafts[item.id] ?? ''),
|
||||
})),
|
||||
deliveryTerms: deliveryTermsDraft.value.trim(),
|
||||
deliveryFee: parseMoneyDraft(deliveryFeeDraft.value) ?? 0,
|
||||
deliveryFee: parseMoneyDraft(deliveryFeeDraft.value),
|
||||
},
|
||||
});
|
||||
await refetchOrder();
|
||||
@@ -290,24 +244,29 @@ function closeDeliveryFeeEditor() {
|
||||
}
|
||||
|
||||
async function openStatusEditor() {
|
||||
if (!canEditStatus.value || statusMutationPending.value) {
|
||||
if (statusMutationPending.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
editingStatus.value = true;
|
||||
statusActionDraft.value = '';
|
||||
statusDraft.value = currentOrder.value?.status ?? '';
|
||||
await focusElement('[data-manager-status-select]');
|
||||
}
|
||||
|
||||
function closeStatusEditor() {
|
||||
if (!statusMutationPending.value) {
|
||||
editingStatus.value = false;
|
||||
statusActionDraft.value = '';
|
||||
statusDraft.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function applyStatusAction() {
|
||||
if (!currentOrder.value || !statusActionDraft.value) {
|
||||
async function applyStatusChange() {
|
||||
if (!currentOrder.value || !statusDraft.value) {
|
||||
closeStatusEditor();
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusDraft.value === currentOrder.value.status) {
|
||||
closeStatusEditor();
|
||||
return;
|
||||
}
|
||||
@@ -315,25 +274,10 @@ async function applyStatusAction() {
|
||||
statusMutationPending.value = true;
|
||||
|
||||
try {
|
||||
if (statusActionDraft.value === 'APPROVE') {
|
||||
await finalizeOrderMutation.mutate({
|
||||
orderId: currentOrder.value.id,
|
||||
decision: Decision.Approve,
|
||||
});
|
||||
}
|
||||
else if (statusActionDraft.value === 'REJECT') {
|
||||
await finalizeOrderMutation.mutate({
|
||||
orderId: currentOrder.value.id,
|
||||
decision: Decision.Reject,
|
||||
});
|
||||
}
|
||||
else if (statusActionDraft.value === 'START') {
|
||||
await startWorkMutation.mutate({ orderId: currentOrder.value.id });
|
||||
}
|
||||
else if (statusActionDraft.value === 'COMPLETE') {
|
||||
await completeOrderMutation.mutate({ orderId: currentOrder.value.id });
|
||||
}
|
||||
|
||||
await setOrderStatusMutation.mutate({
|
||||
orderId: currentOrder.value.id,
|
||||
status: statusDraft.value,
|
||||
});
|
||||
await refetchOrder();
|
||||
}
|
||||
finally {
|
||||
@@ -343,14 +287,14 @@ async function applyStatusAction() {
|
||||
}
|
||||
|
||||
watch(
|
||||
[offerReady, offerSignature, publishedSignature],
|
||||
([ready, nextSignature, currentSignature]) => {
|
||||
[offerSignature, publishedSignature],
|
||||
([nextSignature, currentSignature]) => {
|
||||
if (autosaveTimer) {
|
||||
clearTimeout(autosaveTimer);
|
||||
autosaveTimer = null;
|
||||
}
|
||||
|
||||
if (!ready || !canEditOffer.value || !nextSignature || nextSignature === currentSignature || autosavePending.value) {
|
||||
if (!canEditOffer.value || !nextSignature || nextSignature === currentSignature || autosavePending.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -381,55 +325,35 @@ watch(
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="surface-card rounded-3xl p-5">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div class="space-y-2">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]">Статус заказа</p>
|
||||
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]">Статус заказа</p>
|
||||
|
||||
<label v-if="editingStatus && canEditStatus" class="block">
|
||||
<select
|
||||
v-model="statusActionDraft"
|
||||
data-manager-status-select
|
||||
class="w-full min-w-[220px] rounded-2xl bg-[#f3f5f4] px-4 py-3 text-sm font-semibold text-[#123824] outline-none transition focus:shadow-[0_0_0_3px_rgba(19,153,87,0.12)]"
|
||||
:disabled="statusMutationPending"
|
||||
@change="void applyStatusAction()"
|
||||
@blur="closeStatusEditor"
|
||||
>
|
||||
<option value="" disabled>Выберите статус</option>
|
||||
<option
|
||||
v-for="option in statusActions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="rounded-2xl px-0 py-0 text-left"
|
||||
:class="canEditStatus ? 'cursor-pointer' : 'cursor-default'"
|
||||
@dblclick="void openStatusEditor()"
|
||||
<label v-if="editingStatus" class="mt-3 block">
|
||||
<select
|
||||
v-model="statusDraft"
|
||||
data-manager-status-select
|
||||
class="w-full min-w-[220px] rounded-2xl bg-[#f3f5f4] px-4 py-3 text-sm font-semibold text-[#123824] outline-none transition focus:shadow-[0_0_0_3px_rgba(19,153,87,0.12)]"
|
||||
:disabled="statusMutationPending"
|
||||
@change="void applyStatusChange()"
|
||||
@blur="closeStatusEditor"
|
||||
>
|
||||
<option
|
||||
v-for="option in statusOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
<OrdersOrderStatusBadge :status="currentOrder.status" />
|
||||
</button>
|
||||
</div>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<p class="text-sm text-[#5c7b69]">
|
||||
{{
|
||||
statusMutationPending
|
||||
? 'Обновляем статус...'
|
||||
: canEditStatus
|
||||
? 'Двойной клик по статусу, чтобы изменить.'
|
||||
: 'Статус уже финальный.'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 max-w-2xl text-sm leading-6 text-[#355947]">
|
||||
{{ managerStatusSummary }}
|
||||
</p>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="mt-3 rounded-2xl px-0 py-0 text-left"
|
||||
@dblclick="void openStatusEditor()"
|
||||
>
|
||||
<OrdersOrderStatusBadge :status="currentOrder.status" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -515,16 +439,14 @@ watch(
|
||||
<p class="text-sm text-[#5c7b69]">
|
||||
{{
|
||||
offerTotal == null
|
||||
? 'Итог по заказу появится после заполнения всех цен.'
|
||||
? currentOrder.totalPrice == null
|
||||
? 'Итог пока не задан.'
|
||||
: `Текущий итог: ${formatPrice(currentOrder.totalPrice)}`
|
||||
: autosavePending
|
||||
? `Сохраняем: ${formatPrice(offerTotal)}`
|
||||
: `Текущий итог: ${formatPrice(offerTotal)}`
|
||||
}}
|
||||
</p>
|
||||
|
||||
<p v-if="canEditOffer" class="text-xs font-semibold uppercase tracking-[0.14em] text-[#6a8a76]">
|
||||
Двойной клик по цене или условиям, чтобы изменить.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user