Stage 6 (part 1): new client mini-app look and order ratings
Plum hero with a club-card ticket, a 'Now' block with the live order timeline, new order cards and order screen, QR club card screen, new nav (Home / Orders / Chat / Card / Profile). Clients rate completed orders (1-5 stars + comment); provider rating is recalculated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8866616782
commit
ff527ad6d2
18 files changed
+1536
-205
No files matched your search
@@ -19,3 +19,4 @@ export * from './sales.ts';
|
||||
export * from './texts.ts';
|
||||
export * from './promo.ts';
|
||||
export * from './partners.ts';
|
||||
export * from './ratings.ts';
|
||||
@@ -12,6 +12,7 @@ const clientOrderInclude = {
|
||||
history: { orderBy: { createdAt: 'asc' } },
|
||||
checklist: { orderBy: { sortOrder: 'asc' } },
|
||||
paymentMethod: true,
|
||||
rating: { select: { score: true, comment: true } },
|
||||
} satisfies Prisma.OrderInclude;
|
||||
|
||||
type ClientOrderRow = Prisma.OrderGetPayload<{ include: typeof clientOrderInclude }>;
|
||||
@@ -27,6 +28,9 @@ function clientOrderView(o: ClientOrderRow) {
|
||||
flow,
|
||||
status: o.status,
|
||||
active: !isTerminal(flow, o.status),
|
||||
/** заказ выполнен — можно оценить */
|
||||
done: getFlow(flow).done.includes(o.status),
|
||||
rating: o.rating,
|
||||
scheduledAt: o.scheduledAt,
|
||||
durationMinutes: o.durationMinutes,
|
||||
isOnline: o.isOnline,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { getFlow, type ServiceFlow } from '@barev/core';
|
||||
import { DomainError, type Ctx } from './context.ts';
|
||||
|
||||
/**
|
||||
* Оценка клиента после выполненного заказа: 1–5 звёзд и комментарий.
|
||||
* Повторная оценка заменяет прежнюю; рейтинг исполнителя (ratingSum/ratingCount) пересчитывается.
|
||||
*/
|
||||
export async function rateOrder(ctx: Ctx, input: { orderId: string; clientId: string; score: number; comment?: string | null }) {
|
||||
const score = Math.round(input.score);
|
||||
if (score < 1 || score > 5) throw new DomainError('INVALID_PARAMS');
|
||||
const comment = input.comment?.trim() || null;
|
||||
return ctx.db.$transaction(async (tx) => {
|
||||
const order = await tx.order.findUnique({ where: { id: input.orderId }, include: { service: true, rating: true } });
|
||||
if (!order || order.clientId !== input.clientId) throw new DomainError('NOT_FOUND');
|
||||
if (!getFlow(order.service.flow as ServiceFlow).done.includes(order.status)) throw new DomainError('TRANSITION_NOT_ALLOWED');
|
||||
const prev = order.rating;
|
||||
const providerId = prev?.providerId ?? order.providerId;
|
||||
const rating = prev
|
||||
? await tx.rating.update({ where: { id: prev.id }, data: { score, comment } })
|
||||
: await tx.rating.create({ data: { orderId: order.id, clientId: input.clientId, providerId, score, comment } });
|
||||
if (providerId) {
|
||||
await tx.provider.update({
|
||||
where: { id: providerId },
|
||||
data: prev ? { ratingSum: { increment: score - prev.score } } : { ratingSum: { increment: score }, ratingCount: { increment: 1 } },
|
||||
});
|
||||
}
|
||||
return { score: rating.score, comment: rating.comment };
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user