Simplify order statuses and manager actions
This commit is contained in:
@@ -4,15 +4,12 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const statusLabel = computed(() => {
|
const statusLabel = computed(() => {
|
||||||
if (props.status === 'NEW') return 'Уточнение цены';
|
if (props.status === 'NEW' || props.status === 'MANAGER_PROCESSING') return 'Заявка';
|
||||||
if (props.status === 'MANAGER_PROCESSING') return 'В работе у менеджера';
|
if (props.status === 'WAITING_DOUBLE_CONFIRM' || props.status === 'CONFIRMED') return 'Предложение';
|
||||||
if (props.status === 'WAITING_DOUBLE_CONFIRM') return 'Ожидает подтверждения';
|
if (props.status === 'IN_PROGRESS') return 'В работе';
|
||||||
if (props.status === 'CONFIRMED') return 'Подтвержден';
|
|
||||||
if (props.status === 'IN_PROGRESS') return 'Выполняется';
|
|
||||||
if (props.status === 'COMPLETED') return 'Завершен';
|
if (props.status === 'COMPLETED') return 'Завершен';
|
||||||
if (props.status === 'CLIENT_REJECTED') return 'Отклонен клиентом';
|
if (props.status === 'CLIENT_REJECTED' || props.status === 'MANAGER_REJECTED') return 'Отклонен';
|
||||||
if (props.status === 'MANAGER_REJECTED') return 'Отклонен менеджером';
|
if (props.status === 'MANAGER_BLOCKED') return 'Пауза';
|
||||||
if (props.status === 'MANAGER_BLOCKED') return 'Заблокирован';
|
|
||||||
return props.status;
|
return props.status;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,7 +17,7 @@ const className = computed(() => {
|
|||||||
if (props.status === 'COMPLETED') return 'badge badge-success';
|
if (props.status === 'COMPLETED') return 'badge badge-success';
|
||||||
if (props.status === 'CLIENT_REJECTED' || props.status === 'MANAGER_REJECTED') return 'badge badge-error';
|
if (props.status === 'CLIENT_REJECTED' || props.status === 'MANAGER_REJECTED') return 'badge badge-error';
|
||||||
if (props.status === 'MANAGER_BLOCKED') return 'badge badge-warning';
|
if (props.status === 'MANAGER_BLOCKED') return 'badge badge-warning';
|
||||||
if (props.status === 'NEW') return 'badge badge-warning';
|
if (props.status === 'NEW' || props.status === 'MANAGER_PROCESSING') return 'badge badge-warning';
|
||||||
return 'badge badge-info';
|
return 'badge badge-info';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import { getOrderStatusPresentation } from '~/composables/useOrderStatusPresenta
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
status: string;
|
status: string;
|
||||||
createdAt: string | Date;
|
createdAt: string | Date;
|
||||||
|
audience?: 'client' | 'manager';
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isExpanded = ref(false);
|
const isExpanded = ref(false);
|
||||||
|
|
||||||
const presentation = computed(() => getOrderStatusPresentation(props.status, props.createdAt));
|
const presentation = computed(() => getOrderStatusPresentation(props.status, props.createdAt, props.audience ?? 'client'));
|
||||||
|
|
||||||
function itemClass(state: 'done' | 'current' | 'upcoming') {
|
function itemClass(state: 'done' | 'current' | 'upcoming') {
|
||||||
if (state === 'current') {
|
if (state === 'current') {
|
||||||
|
|||||||
@@ -65,25 +65,99 @@ function buildDates(createdAt: string | Date) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOrderStatusPresentation(status: string, createdAt: string | Date): StatusPresentation {
|
function buildManagerStages(status: string, createdAt: string | Date): StatusPresentation {
|
||||||
const dates = buildDates(createdAt);
|
const dates = buildDates(createdAt);
|
||||||
|
|
||||||
if (status === 'CLIENT_REJECTED') {
|
const isOfferStage = ['WAITING_DOUBLE_CONFIRM', 'CONFIRMED'].includes(status);
|
||||||
|
const isWorkStage = ['IN_PROGRESS', 'COMPLETED'].includes(status);
|
||||||
|
const isStopped = ['CLIENT_REJECTED', 'MANAGER_REJECTED', 'MANAGER_BLOCKED'].includes(status);
|
||||||
|
|
||||||
|
const stages: TimelineStage[] = [
|
||||||
|
{
|
||||||
|
code: 'NEW',
|
||||||
|
label: 'Заявка',
|
||||||
|
note: 'Заказ создан и ждёт расчёта.',
|
||||||
|
dateLabel: formatDay(dates.created),
|
||||||
|
state: isOfferStage || isWorkStage || isStopped ? 'done' : 'current',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'WAITING_DOUBLE_CONFIRM',
|
||||||
|
label: 'Предложение',
|
||||||
|
note: 'Цена и условия опубликованы клиенту.',
|
||||||
|
dateLabel: formatDay(dates.offer),
|
||||||
|
state: isWorkStage ? 'done' : isOfferStage ? 'current' : 'upcoming',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'IN_PROGRESS',
|
||||||
|
label: status === 'COMPLETED' ? 'Исполнение завершено' : 'В работе',
|
||||||
|
note: status === 'COMPLETED'
|
||||||
|
? 'Заказ передан в работу и закрыт.'
|
||||||
|
: 'Заказ передан в производство или исполнение.',
|
||||||
|
dateLabel: formatDay(dates.production),
|
||||||
|
state: isWorkStage ? 'current' : 'upcoming',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (status === 'MANAGER_BLOCKED') {
|
||||||
return {
|
return {
|
||||||
title: 'Заказ остановлен клиентом',
|
title: 'Заказ на паузе',
|
||||||
summary: 'Согласование остановлено. Если нужно, заказ можно собрать заново с новыми условиями.',
|
summary: 'Нужно уточнение по заказу перед публикацией предложения.',
|
||||||
|
stages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'CLIENT_REJECTED' || status === 'MANAGER_REJECTED') {
|
||||||
|
return {
|
||||||
|
title: 'Заказ остановлен',
|
||||||
|
summary: 'Текущий заказ завершён без запуска в работу.',
|
||||||
|
stages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWorkStage) {
|
||||||
|
return {
|
||||||
|
title: status === 'COMPLETED' ? 'Заказ завершён' : 'Заказ в работе',
|
||||||
|
summary: status === 'COMPLETED'
|
||||||
|
? 'Исполнение завершено, история этапов сохранена.'
|
||||||
|
: 'Предложение согласовано, заказ уже в работе.',
|
||||||
|
stages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOfferStage) {
|
||||||
|
return {
|
||||||
|
title: 'Предложение отправлено',
|
||||||
|
summary: 'Клиент уже видит цену и условия. Следующий шаг для менеджера: пустить заказ в работу.',
|
||||||
|
stages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: 'Ждём расчёт по заявке',
|
||||||
|
summary: 'Заполните цену по позициям и логистике, после этого предложение уйдёт клиенту автоматически.',
|
||||||
|
stages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildClientStages(status: string, createdAt: string | Date): StatusPresentation {
|
||||||
|
const dates = buildDates(createdAt);
|
||||||
|
|
||||||
|
if (status === 'CLIENT_REJECTED' || status === 'MANAGER_REJECTED') {
|
||||||
|
return {
|
||||||
|
title: 'Заказ остановлен',
|
||||||
|
summary: 'Текущий заказ закрыт. При необходимости можно оформить новый заказ.',
|
||||||
stages: [
|
stages: [
|
||||||
{
|
{
|
||||||
code: 'NEW',
|
code: 'NEW',
|
||||||
label: 'Заявка принята',
|
label: 'Заказ создан',
|
||||||
note: 'Заказ попал в обработку.',
|
note: 'Заказ принят в обработку.',
|
||||||
dateLabel: formatDay(dates.created),
|
dateLabel: formatDay(dates.created),
|
||||||
state: 'done',
|
state: 'done',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'CLIENT_REJECTED',
|
code: status,
|
||||||
label: 'Клиент отказался от продолжения',
|
label: 'Заказ остановлен',
|
||||||
note: 'Текущий заказ закрыт без запуска в работу.',
|
note: 'Дальнейшее исполнение не планируется.',
|
||||||
dateLabel: formatDay(dates.approval),
|
dateLabel: formatDay(dates.approval),
|
||||||
state: 'current',
|
state: 'current',
|
||||||
},
|
},
|
||||||
@@ -91,26 +165,22 @@ export function getOrderStatusPresentation(status: string, createdAt: string | D
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'MANAGER_REJECTED' || status === 'MANAGER_BLOCKED') {
|
if (status === 'MANAGER_BLOCKED') {
|
||||||
return {
|
return {
|
||||||
title: status === 'MANAGER_BLOCKED' ? 'Заказ ждёт уточнения' : 'Заказ остановлен менеджером',
|
title: 'Заказ ждёт уточнения',
|
||||||
summary: status === 'MANAGER_BLOCKED'
|
summary: 'Менеджер уточняет параметры заказа перед расчётом.',
|
||||||
? 'Сейчас ждём уточнение параметров, после него заказ вернётся в работу.'
|
|
||||||
: 'Менеджер остановил обработку. При необходимости можно собрать новый заказ.',
|
|
||||||
stages: [
|
stages: [
|
||||||
{
|
{
|
||||||
code: 'NEW',
|
code: 'NEW',
|
||||||
label: 'Заявка принята',
|
label: 'Заказ создан',
|
||||||
note: 'Заказ попал в обработку.',
|
note: 'Заказ принят в обработку.',
|
||||||
dateLabel: formatDay(dates.created),
|
dateLabel: formatDay(dates.created),
|
||||||
state: 'done',
|
state: 'done',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: status,
|
code: status,
|
||||||
label: status === 'MANAGER_BLOCKED' ? 'Нужно уточнение по заказу' : 'Обработка остановлена',
|
label: 'Уточняем детали',
|
||||||
note: status === 'MANAGER_BLOCKED'
|
note: 'После уточнения покажем плановые даты по исполнению.',
|
||||||
? 'Менеджер запросил уточнение перед продолжением.'
|
|
||||||
: 'Текущий заказ завершён без запуска в производство.',
|
|
||||||
dateLabel: formatDay(dates.approval),
|
dateLabel: formatDay(dates.approval),
|
||||||
state: 'current',
|
state: 'current',
|
||||||
},
|
},
|
||||||
@@ -123,76 +193,62 @@ export function getOrderStatusPresentation(status: string, createdAt: string | D
|
|||||||
const stages: TimelineStage[] = [
|
const stages: TimelineStage[] = [
|
||||||
{
|
{
|
||||||
code: 'NEW',
|
code: 'NEW',
|
||||||
label: 'Заявка принята',
|
label: 'Заказ создан',
|
||||||
note: 'Получили состав заказа и начали обработку.',
|
note: 'Приняли заказ и начали обработку.',
|
||||||
dateLabel: formatDay(dates.created),
|
dateLabel: formatDay(dates.created),
|
||||||
state: currentIndex > 0 ? 'done' : 'current',
|
state: currentIndex > 0 ? 'done' : 'current',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
code: 'MANAGER_PROCESSING',
|
|
||||||
label: 'Готовим предложение',
|
|
||||||
note: 'Уточняем цену, сроки и условия доставки.',
|
|
||||||
dateLabel: formatDay(dates.offer),
|
|
||||||
state: currentIndex > 1 ? 'done' : currentIndex === 1 ? 'current' : 'upcoming',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: 'WAITING_DOUBLE_CONFIRM',
|
|
||||||
label: 'Ждём подтверждение',
|
|
||||||
note: 'Показываем согласованные условия и ждём финальное подтверждение.',
|
|
||||||
dateLabel: formatDay(dates.approval),
|
|
||||||
state: currentIndex > 2 ? 'done' : currentIndex === 2 ? 'current' : 'upcoming',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
code: 'CONFIRMED',
|
code: 'CONFIRMED',
|
||||||
label: 'Заказ подтверждён',
|
label: 'Производство',
|
||||||
note: `Планируем передать в производство и ориентируемся на выпуск ${formatDay(dates.production)}.`,
|
note: 'Плановая дата запуска или выхода из производства.',
|
||||||
dateLabel: formatDay(dates.production),
|
dateLabel: formatDay(dates.production),
|
||||||
state: currentIndex > 3 ? 'done' : currentIndex === 3 ? 'current' : 'upcoming',
|
state: currentIndex > 3 ? 'done' : currentIndex >= 3 ? 'current' : 'upcoming',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'IN_PROGRESS',
|
code: 'IN_PROGRESS',
|
||||||
label: 'Заказ в работе',
|
label: 'Отгрузка',
|
||||||
note: `Сейчас идёт исполнение. Следующее ожидаемое обновление около ${formatDay(dates.shipment)}.`,
|
note: 'Плановая дата передачи в логистику.',
|
||||||
dateLabel: formatDay(dates.shipment),
|
dateLabel: formatDay(dates.shipment),
|
||||||
state: currentIndex > 4 ? 'done' : currentIndex === 4 ? 'current' : 'upcoming',
|
state: currentIndex > 4 ? 'done' : currentIndex >= 4 ? 'current' : 'upcoming',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'COMPLETED',
|
code: 'COMPLETED',
|
||||||
label: 'Заказ завершён',
|
label: 'Доставка',
|
||||||
note: 'Исполнение закрыто, заказ готов к выдаче или уже доставлен.',
|
note: 'Плановая дата получения заказа.',
|
||||||
dateLabel: formatDay(dates.delivered),
|
dateLabel: formatDay(dates.delivered),
|
||||||
state: currentIndex === 5 ? 'current' : currentIndex > 5 ? 'done' : 'upcoming',
|
state: currentIndex >= 5 ? 'current' : 'upcoming',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (status === 'NEW') {
|
if (status === 'NEW') {
|
||||||
return {
|
return {
|
||||||
title: 'Собираем предложение по заказу',
|
title: 'Заказ создан',
|
||||||
summary: `Сейчас уточняем стоимость и комплектацию. Следующее обновление ориентировочно ${formatDay(dates.offer)}.`,
|
summary: 'Менеджер рассчитывает стоимость и готовит план по исполнению.',
|
||||||
stages,
|
stages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'MANAGER_PROCESSING') {
|
if (status === 'MANAGER_PROCESSING') {
|
||||||
return {
|
return {
|
||||||
title: 'Готовим условия по заказу',
|
title: 'Готовим предложение',
|
||||||
summary: `Менеджер собирает финальные условия. Ориентир по следующему апдейту ${formatDay(dates.approval)}.`,
|
summary: 'Собираем итоговые условия и скоро покажем плановые даты.',
|
||||||
stages,
|
stages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'WAITING_DOUBLE_CONFIRM') {
|
if (status === 'WAITING_DOUBLE_CONFIRM') {
|
||||||
return {
|
return {
|
||||||
title: 'Ожидаем подтверждение условий',
|
title: 'Предложение готово',
|
||||||
summary: 'Нужно подтвердить текущие условия, после этого сразу двинем заказ дальше.',
|
summary: 'Стоимость и условия уже рассчитаны. Следующий этап после запуска: производство.',
|
||||||
stages,
|
stages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'CONFIRMED') {
|
if (status === 'CONFIRMED') {
|
||||||
return {
|
return {
|
||||||
title: 'Заказ подтверждён',
|
title: 'Планируем производство',
|
||||||
summary: `Ожидаем выпуск с производства ориентировочно ${formatDay(dates.production)}.`,
|
summary: `Ориентируемся на производство ${formatDay(dates.production)}, затем отгрузку и доставку по плану.`,
|
||||||
stages,
|
stages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -200,14 +256,24 @@ export function getOrderStatusPresentation(status: string, createdAt: string | D
|
|||||||
if (status === 'IN_PROGRESS') {
|
if (status === 'IN_PROGRESS') {
|
||||||
return {
|
return {
|
||||||
title: 'Заказ в работе',
|
title: 'Заказ в работе',
|
||||||
summary: `Сейчас заказ исполняется. Следующее ожидаемое обновление около ${formatDay(dates.shipment)}.`,
|
summary: `Производство идёт. Следующая плановая дата: отгрузка около ${formatDay(dates.shipment)}.`,
|
||||||
stages,
|
stages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: 'Заказ завершён',
|
title: 'Доставка по плану',
|
||||||
summary: 'Работы по заказу завершены, история этапов сохранена ниже.',
|
summary: `Финальный ориентир по заказу: доставка ${formatDay(dates.delivered)}.`,
|
||||||
stages,
|
stages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getOrderStatusPresentation(
|
||||||
|
status: string,
|
||||||
|
createdAt: string | Date,
|
||||||
|
audience: 'client' | 'manager' = 'client',
|
||||||
|
): StatusPresentation {
|
||||||
|
return audience === 'manager'
|
||||||
|
? buildManagerStages(status, createdAt)
|
||||||
|
: buildClientStages(status, createdAt);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useMutation, useQuery } from '@vue/apollo-composable';
|
import { useMutation, useQuery } from '@vue/apollo-composable';
|
||||||
import {
|
import {
|
||||||
BlockOrderDocument,
|
|
||||||
CompleteOrderDocument,
|
|
||||||
ManagerFinalizeOrderDocument,
|
|
||||||
ManagerSetOrderOfferDocument,
|
ManagerSetOrderOfferDocument,
|
||||||
OrderDetailDocument,
|
OrderDetailDocument,
|
||||||
StartOrderWorkDocument,
|
StartOrderWorkDocument,
|
||||||
@@ -29,15 +26,14 @@ const orderQuery = useQuery(OrderDetailDocument, () => ({
|
|||||||
id: orderId.value,
|
id: orderId.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const finalizeMutation = useMutation(ManagerFinalizeOrderDocument);
|
|
||||||
const blockMutation = useMutation(BlockOrderDocument);
|
|
||||||
const startWorkMutation = useMutation(StartOrderWorkDocument);
|
const startWorkMutation = useMutation(StartOrderWorkDocument);
|
||||||
const completeWorkMutation = useMutation(CompleteOrderDocument);
|
|
||||||
const setOfferMutation = useMutation(ManagerSetOrderOfferDocument);
|
const setOfferMutation = useMutation(ManagerSetOrderOfferDocument);
|
||||||
|
|
||||||
const itemPriceDrafts = reactive<Record<string, string>>({});
|
const itemPriceDrafts = reactive<Record<string, string>>({});
|
||||||
const deliveryTermsDraft = ref('');
|
const deliveryTermsDraft = ref('');
|
||||||
const deliveryFeeDraft = ref('');
|
const deliveryFeeDraft = ref('');
|
||||||
|
const autosavePending = ref(false);
|
||||||
|
let autosaveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
const currentOrder = computed<ManagerOrderItem | null>(() =>
|
const currentOrder = computed<ManagerOrderItem | null>(() =>
|
||||||
orderQuery.result.value?.order ?? null,
|
orderQuery.result.value?.order ?? null,
|
||||||
@@ -95,6 +91,40 @@ function draftLineTotal(item: ManagerOrderItem['items'][number]) {
|
|||||||
|
|
||||||
const draftDeliveryTerms = computed(() => deliveryTermsDraft.value.trim() || currentOrder.value?.deliveryTerms || null);
|
const draftDeliveryTerms = computed(() => deliveryTermsDraft.value.trim() || currentOrder.value?.deliveryTerms || null);
|
||||||
const draftDeliveryFee = computed(() => parseMoneyDraft(deliveryFeeDraft.value) ?? currentOrder.value?.deliveryFee ?? 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 offerSignature = computed(() => {
|
||||||
|
if (!currentOrder.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
deliveryTerms: deliveryTermsDraft.value.trim(),
|
||||||
|
deliveryFee: parseMoneyDraft(deliveryFeeDraft.value),
|
||||||
|
itemPrices: currentOrder.value.items.map((item) => ({
|
||||||
|
itemId: item.id,
|
||||||
|
unitPrice: parseMoneyDraft(itemPriceDrafts[item.id] ?? ''),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const publishedSignature = computed(() => {
|
||||||
|
if (!currentOrder.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
deliveryTerms: currentOrder.value.deliveryTerms ?? '',
|
||||||
|
deliveryFee: currentOrder.value.deliveryFee ?? null,
|
||||||
|
itemPrices: currentOrder.value.items.map((item) => ({
|
||||||
|
itemId: item.id,
|
||||||
|
unitPrice: item.unitPrice ?? null,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const offerReady = computed(() => {
|
const offerReady = computed(() => {
|
||||||
if (!currentOrder.value) {
|
if (!currentOrder.value) {
|
||||||
@@ -126,54 +156,28 @@ async function refetchOrder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveOffer() {
|
async function saveOffer() {
|
||||||
if (!currentOrder.value || !offerReady.value) {
|
if (!currentOrder.value || !offerReady.value || !canEditOffer.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await setOfferMutation.mutate({
|
autosavePending.value = true;
|
||||||
input: {
|
try {
|
||||||
orderId: currentOrder.value.id,
|
await setOfferMutation.mutate({
|
||||||
itemPrices: currentOrder.value.items.map((item) => ({
|
input: {
|
||||||
itemId: item.id,
|
orderId: currentOrder.value.id,
|
||||||
unitPrice: parseMoneyDraft(itemPriceDrafts[item.id] ?? '') ?? 0,
|
itemPrices: currentOrder.value.items.map((item) => ({
|
||||||
})),
|
itemId: item.id,
|
||||||
deliveryTerms: deliveryTermsDraft.value.trim(),
|
unitPrice: parseMoneyDraft(itemPriceDrafts[item.id] ?? '') ?? 0,
|
||||||
deliveryFee: parseMoneyDraft(deliveryFeeDraft.value) ?? 0,
|
})),
|
||||||
},
|
deliveryTerms: deliveryTermsDraft.value.trim(),
|
||||||
});
|
deliveryFee: parseMoneyDraft(deliveryFeeDraft.value) ?? 0,
|
||||||
await refetchOrder();
|
},
|
||||||
}
|
});
|
||||||
|
await refetchOrder();
|
||||||
async function approveOrder() {
|
|
||||||
if (!currentOrder.value) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
await finalizeMutation.mutate({ orderId: currentOrder.value.id, decision: 'APPROVE' });
|
autosavePending.value = false;
|
||||||
await refetchOrder();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rejectOrder() {
|
|
||||||
if (!currentOrder.value) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await finalizeMutation.mutate({ orderId: currentOrder.value.id, decision: 'REJECT' });
|
|
||||||
await refetchOrder();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function blockOrder() {
|
|
||||||
if (!currentOrder.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await blockMutation.mutate({
|
|
||||||
input: {
|
|
||||||
orderId: currentOrder.value.id,
|
|
||||||
reason: 'Нужно уточнение параметров заказа.',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await refetchOrder();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startOrder() {
|
async function startOrder() {
|
||||||
@@ -185,14 +189,23 @@ async function startOrder() {
|
|||||||
await refetchOrder();
|
await refetchOrder();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function completeOrder() {
|
watch(
|
||||||
if (!currentOrder.value) {
|
[offerReady, offerSignature, publishedSignature],
|
||||||
return;
|
([ready, nextSignature, currentSignature]) => {
|
||||||
}
|
if (autosaveTimer) {
|
||||||
|
clearTimeout(autosaveTimer);
|
||||||
|
autosaveTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
await completeWorkMutation.mutate({ orderId: currentOrder.value.id });
|
if (!ready || !canEditOffer.value || !nextSignature || nextSignature === currentSignature || autosavePending.value) {
|
||||||
await refetchOrder();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
autosaveTimer = setTimeout(() => {
|
||||||
|
void saveOffer();
|
||||||
|
}, 700);
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -217,6 +230,7 @@ async function completeOrder() {
|
|||||||
<OrdersOrderStatusTimelineCard
|
<OrdersOrderStatusTimelineCard
|
||||||
:status="currentOrder.status"
|
:status="currentOrder.status"
|
||||||
:created-at="currentOrder.createdAt"
|
:created-at="currentOrder.createdAt"
|
||||||
|
audience="manager"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="surface-card rounded-3xl p-5">
|
<div class="surface-card rounded-3xl p-5">
|
||||||
@@ -237,6 +251,7 @@ async function completeOrder() {
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
placeholder="Например, 125.50"
|
placeholder="Например, 125.50"
|
||||||
class="input input-bordered w-full rounded-2xl bg-white"
|
class="input input-bordered w-full rounded-2xl bg-white"
|
||||||
|
:disabled="!canEditOffer"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
@@ -258,6 +273,7 @@ async function completeOrder() {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="Например, доставка до склада 2-3 дня"
|
placeholder="Например, доставка до склада 2-3 дня"
|
||||||
class="input input-bordered w-full rounded-2xl bg-white"
|
class="input input-bordered w-full rounded-2xl bg-white"
|
||||||
|
:disabled="!canEditOffer"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -272,6 +288,7 @@ async function completeOrder() {
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
placeholder="Например, 3000"
|
placeholder="Например, 3000"
|
||||||
class="input input-bordered w-full rounded-2xl bg-white"
|
class="input input-bordered w-full rounded-2xl bg-white"
|
||||||
|
:disabled="!canEditOffer"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,29 +299,21 @@ async function completeOrder() {
|
|||||||
{{
|
{{
|
||||||
offerTotal == null
|
offerTotal == null
|
||||||
? 'Итог по заказу посчитается автоматически после заполнения всех цен.'
|
? 'Итог по заказу посчитается автоматически после заполнения всех цен.'
|
||||||
: `Предварительный итог: ${formatPrice(offerTotal)}`
|
: autosavePending
|
||||||
|
? `Сохраняем и отправляем клиенту: ${formatPrice(offerTotal)}`
|
||||||
|
: `Предложение отправится клиенту автоматически: ${formatPrice(offerTotal)}`
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
<button
|
|
||||||
class="btn btn-primary rounded-full border-0 px-5"
|
|
||||||
:disabled="!offerReady || setOfferMutation.loading.value"
|
|
||||||
@click="saveOffer"
|
|
||||||
>
|
|
||||||
Сохранить условия
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="surface-card rounded-3xl p-5">
|
<div v-if="['WAITING_DOUBLE_CONFIRM', 'CONFIRMED'].includes(currentOrder.status)" class="surface-card rounded-3xl p-5">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<h2 class="text-xl font-bold text-[#123824]">Действия менеджера</h2>
|
<div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<h2 class="text-xl font-bold text-[#123824]">Следующий шаг</h2>
|
||||||
<button class="btn btn-success btn-sm border-0" @click="approveOrder">Подтвердить</button>
|
<p class="mt-1 text-sm text-[#5c7b69]">Когда всё согласовано, менеджер просто переводит заказ в работу.</p>
|
||||||
<button class="btn btn-error btn-sm border-0" @click="rejectOrder">Отклонить</button>
|
|
||||||
<button class="btn btn-warning btn-sm border-0" @click="blockOrder">Заблокировать</button>
|
|
||||||
<button class="btn btn-accent btn-sm border-0" :disabled="currentOrder.status !== 'CONFIRMED'" @click="startOrder">В работу</button>
|
|
||||||
<button class="btn btn-neutral btn-sm border-0" :disabled="currentOrder.status !== 'IN_PROGRESS'" @click="completeOrder">Завершить</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btn-accent rounded-full border-0 px-5" @click="startOrder">Пустить в работу</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -119,12 +119,12 @@ const statusTabs = computed<Array<{ id: StatusFilter; label: string; count: numb
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'NEW',
|
id: 'NEW',
|
||||||
label: 'Новые',
|
label: 'Заявки',
|
||||||
count: searchedOrders.value.filter((order) => getOrderGroup(order) === 'NEW').length,
|
count: searchedOrders.value.filter((order) => getOrderGroup(order) === 'NEW').length,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'PRICED',
|
id: 'PRICED',
|
||||||
label: 'Оценены',
|
label: 'Предложения',
|
||||||
count: searchedOrders.value.filter((order) => getOrderGroup(order) === 'PRICED').length,
|
count: searchedOrders.value.filter((order) => getOrderGroup(order) === 'PRICED').length,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -143,12 +143,12 @@ const kanbanColumns = computed(() => {
|
|||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
id: 'NEW' as const,
|
id: 'NEW' as const,
|
||||||
title: 'Новые',
|
title: 'Заявки',
|
||||||
tone: 'from-[#f5fff8] to-[#eefaf2]',
|
tone: 'from-[#f5fff8] to-[#eefaf2]',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'PRICED' as const,
|
id: 'PRICED' as const,
|
||||||
title: 'Оценены',
|
title: 'Предложения',
|
||||||
tone: 'from-[#fff9ef] to-[#fff1d9]',
|
tone: 'from-[#fff9ef] to-[#fff1d9]',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ const currentOrder = computed<OrderItem | null>(() =>
|
|||||||
<OrdersOrderStatusTimelineCard
|
<OrdersOrderStatusTimelineCard
|
||||||
:status="currentOrder.status"
|
:status="currentOrder.status"
|
||||||
:created-at="currentOrder.createdAt"
|
:created-at="currentOrder.createdAt"
|
||||||
|
audience="client"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="surface-card rounded-3xl p-5">
|
<div class="surface-card rounded-3xl p-5">
|
||||||
|
|||||||
Reference in New Issue
Block a user