fix: keep deal edits and right-align club leaderboard

Empty product lists were wiping the manual amount on update, and follow-ups were only created for new deals. Leaderboard rows were laid out left-to-right on an RTL page.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-13 11:38:59 +03:30
co-authored by Cursor
parent 6b4bd0b03e
commit 0e420a443f
3 changed files with 27 additions and 14 deletions
+4 -3
View File
@@ -225,14 +225,15 @@ export class DealsService {
} }
const data: Prisma.DealUncheckedUpdateInput = {}; const data: Prisma.DealUncheckedUpdateInput = {};
const pricedFromLines = (dto.lineItems?.length ?? 0) > 0;
if (dto.title !== undefined) data.title = dto.title.trim(); if (dto.title !== undefined) data.title = dto.title.trim();
if ('amount' in dto && dto.lineItems === undefined) { if ('amount' in dto && !pricedFromLines) {
data.amount = data.amount =
dto.amount == null || Number.isNaN(Number(dto.amount)) dto.amount == null || Number.isNaN(Number(dto.amount))
? null ? null
: new Prisma.Decimal(dto.amount); : new Prisma.Decimal(dto.amount);
} }
if (dto.currency !== undefined && dto.lineItems === undefined) { if (dto.currency !== undefined && !pricedFromLines) {
data.currency = dto.currency.trim() || 'IRR'; data.currency = dto.currency.trim() || 'IRR';
} }
if ('source' in dto) data.source = emptyToNull(dto.source); if ('source' in dto) data.source = emptyToNull(dto.source);
@@ -270,7 +271,7 @@ export class DealsService {
assigneeConnect = assigneeIds.map((uid) => ({ id: uid })); assigneeConnect = assigneeIds.map((uid) => ({ id: uid }));
} }
if (dto.lineItems !== undefined) { if (pricedFromLines && dto.lineItems) {
const total = sumLineItems(dto.lineItems); const total = sumLineItems(dto.lineItems);
data.amount = data.amount =
total == null ? null : new Prisma.Decimal(total); total == null ? null : new Prisma.Decimal(total);
@@ -97,26 +97,26 @@ const meOutside = computed(() => {
<ol class="rows" :class="{ loading }"> <ol class="rows" :class="{ loading }">
<li v-for="e in rest" :key="e.userId" :class="{ me: e.isMe }"> <li v-for="e in rest" :key="e.userId" :class="{ me: e.isMe }">
<span class="rank">{{ toFaDigits(e.rank) }}</span>
<UserAvatar :user-id="e.userId" :has-avatar="e.hasAvatar" :name="e.fullName" :size="28" />
<b class="who">{{ e.isMe ? 'شما' : e.fullName }}</b>
<span class="score">{{ faNum(e.score) }}</span>
<span class="chg" :class="chgClass(e.change)"> <span class="chg" :class="chgClass(e.change)">
<template v-if="e.change">{{ e.change > 0 ? '▲' : '▼' }} {{ toFaDigits(Math.abs(e.change)) }}</template> <template v-if="e.change">{{ e.change > 0 ? '▲' : '▼' }} {{ toFaDigits(Math.abs(e.change)) }}</template>
</span> </span>
<span class="score">{{ faNum(e.score) }}</span>
<b class="who">{{ e.isMe ? 'شما' : e.fullName }}</b>
<UserAvatar :user-id="e.userId" :has-avatar="e.hasAvatar" :name="e.fullName" :size="28" />
<span class="rank">{{ toFaDigits(e.rank) }}</span>
</li> </li>
<template v-if="meOutside"> <template v-if="meOutside">
<li class="sep" aria-hidden="true">⋯</li> <li class="sep" aria-hidden="true">⋯</li>
<li class="me"> <li class="me">
<span class="rank">{{ toFaDigits(meOutside.rank) }}</span>
<UserAvatar :user-id="meOutside.userId" :has-avatar="meOutside.hasAvatar" :name="meOutside.fullName" :size="28" />
<b class="who">شما</b>
<span class="score">{{ faNum(meOutside.score) }}</span>
<span class="chg" :class="chgClass(meOutside.change)"> <span class="chg" :class="chgClass(meOutside.change)">
<template v-if="meOutside.change"> <template v-if="meOutside.change">
{{ meOutside.change > 0 ? '▲' : '▼' }} {{ toFaDigits(Math.abs(meOutside.change)) }} {{ meOutside.change > 0 ? '▲' : '▼' }} {{ toFaDigits(Math.abs(meOutside.change)) }}
</template> </template>
</span> </span>
<span class="score">{{ faNum(meOutside.score) }}</span>
<b class="who">شما</b>
<UserAvatar :user-id="meOutside.userId" :has-avatar="meOutside.hasAvatar" :name="meOutside.fullName" :size="28" />
<span class="rank">{{ toFaDigits(meOutside.rank) }}</span>
</li> </li>
</template> </template>
<li v-if="board && !board.items.length" class="empty">هنوز کسی در این بازه امتیاز نگرفته است.</li> <li v-if="board && !board.items.length" class="empty">هنوز کسی در این بازه امتیاز نگرفته است.</li>
@@ -315,7 +315,7 @@ const meOutside = computed(() => {
.who { .who {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
text-align: end; text-align: start;
font-size: 0.84rem; font-size: 0.84rem;
font-weight: 600; font-weight: 600;
color: #0f172a; color: #0f172a;
+14 -2
View File
@@ -116,6 +116,8 @@ const sourceRef = ref('');
const status = ref(''); const status = ref('');
const customFields = ref<Record<string, unknown>>({}); const customFields = ref<Record<string, unknown>>({});
const lines = ref<LineDraft[]>([]); const lines = ref<LineDraft[]>([]);
/** تعداد اقلام ذخیره‌شده؛ برای حذف همهٔ اقلام باید آرایهٔ خالی فرستاده شود */
const savedLineCount = ref(0);
const nextDueDate = ref(''); const nextDueDate = ref('');
const nextDueTime = ref('10:30'); const nextDueTime = ref('10:30');
@@ -364,7 +366,11 @@ async function load() {
quantity: String(item.quantity), quantity: String(item.quantity),
unitPrice: String(item.unitPrice), unitPrice: String(item.unitPrice),
})); }));
savedLineCount.value = lines.value.length;
productsOpen.value = lines.value.length > 0; productsOpen.value = lines.value.length > 0;
if (data.company && !companies.value.some((c) => c.id === data.company!.id)) {
companies.value = [{ id: data.company.id, name: data.company.name } as CompanyDto, ...companies.value];
}
if (data.pipelineId !== pipelineId.value) { if (data.pipelineId !== pipelineId.value) {
await loadBoard(data.pipelineId); await loadBoard(data.pipelineId);
stageId.value = data.stageId; stageId.value = data.stageId;
@@ -397,6 +403,7 @@ async function load() {
function payload(): CreateDealRequest { function payload(): CreateDealRequest {
const lineItems = toLineInputs(); const lineItems = toLineInputs();
const sendLineItems = lineItems.length > 0 || savedLineCount.value > 0;
return { return {
title: title.value, title: title.value,
amount: amount:
@@ -423,12 +430,12 @@ function payload(): CreateDealRequest {
stageId: stageId.value || undefined, stageId: stageId.value || undefined,
customFields: customFields.value, customFields: customFields.value,
currency: currency.value || 'IRR', currency: currency.value || 'IRR',
lineItems, ...(sendLineItems ? { lineItems } : {}),
}; };
} }
async function createFollowUp(dealId: string) { async function createFollowUp(dealId: string) {
if (!isNew.value || !nextDueDate.value) return; if (!nextDueDate.value) return;
const due = new Date(`${nextDueDate.value}T${nextDueTime.value || '10:30'}:00`); const due = new Date(`${nextDueDate.value}T${nextDueTime.value || '10:30'}:00`);
if (Number.isNaN(due.getTime())) return; if (Number.isNaN(due.getTime())) return;
await api.post('/activities', { await api.post('/activities', {
@@ -488,6 +495,11 @@ async function submit() {
if (moveStageId) { if (moveStageId) {
await api.patch(`/deals/${id.value}/move`, { stageId: moveStageId }); await api.patch(`/deals/${id.value}/move`, { stageId: moveStageId });
} }
try {
await createFollowUp(id.value);
} catch {
/* deal saved; follow-up is optional */
}
if (props.embedded) { if (props.embedded) {
emit('saved'); emit('saved');
return; return;