Add salesperson commissions on issued invoices and a cheque register.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-11 01:09:59 +03:30
co-authored by Cursor
parent 535c428270
commit 9a991fc1f1
22 changed files with 1238 additions and 11 deletions
+153
View File
@@ -14,6 +14,7 @@ import MoneyInput from '@/components/MoneyInput.vue';
import UiIcon from '@/components/UiIcon.vue';
import BrandName from '@/components/brand/BrandName.vue';
import { brand } from '@mento/shared';
import { useAuthStore } from '@/stores/auth';
import type {
CreateInvoiceRequest,
CreatePaymentRequest,
@@ -24,6 +25,7 @@ import type {
InvoiceInstallmentInput,
InvoiceStatus,
OrganizationDto,
OrgUserDto,
PaginatedResponse,
PaymentMethod,
PersonDto,
@@ -125,6 +127,15 @@ const payMethod = ref<PaymentMethod>('transfer');
const payNotes = ref('');
const payInstallmentId = ref('');
const payDate = ref(formatLocalDateInput(new Date()));
const chequeNumber = ref('');
const chequeBank = ref('');
const chequeDue = ref('');
const chequeStatus = ref<'pending' | 'cleared' | 'bounced'>('pending');
const commissionMode = ref<'percent' | 'amount'>('percent');
const commissionPercent = ref('');
const commissionFixedAmount = ref('');
const defaultCommissionPercent = ref(0);
const auth = useAuthStore();
const invoiceStatusLabel: Record<InvoiceStatus, string> = {
draft: 'پیش‌نویس',
@@ -249,6 +260,16 @@ const payableAmount = computed(() =>
docMode.value === 'credit' ? -payableSigned.value : payableSigned.value,
);
const commissionPreview = computed(() => {
const base = Math.abs(payableAmount.value);
if (commissionMode.value === 'amount') {
const n = Number(commissionFixedAmount.value);
return Number.isFinite(n) ? n : 0;
}
const n = Number(commissionPercent.value || defaultCommissionPercent.value);
return Number.isFinite(n) ? Math.round((base * n) / 100) : 0;
});
function formatMoney(amount: number) {
return formatMoneyLib(amount, currency.value);
}
@@ -461,6 +482,9 @@ async function load() {
loadedInvoiceKind.value = null;
installments.value = [];
lines.value = [];
commissionMode.value = 'percent';
commissionPercent.value = '';
commissionFixedAmount.value = '';
validityMode.value = 'days';
validityDays.value = 30;
@@ -473,6 +497,16 @@ async function load() {
]);
people.value = peopleRes.data.items;
products.value = productsRes.data;
try {
const { data: team } = await api.get<OrgUserDto[]>('/users');
const me = team.find((u) => u.id === auth.user?.id);
defaultCommissionPercent.value = Number(me?.commissionPercent ?? 0);
if (!commissionPercent.value && defaultCommissionPercent.value) {
commissionPercent.value = String(defaultCommissionPercent.value);
}
} catch {
/* optional */
}
enabledCurrencies.value = orgRes.data.enabledCurrencies?.length
? orgRes.data.enabledCurrencies
: [orgRes.data.baseCurrency || 'IRR'];
@@ -525,6 +559,18 @@ async function load() {
if (data.remainingAmount > 0) {
payAmount.value = String(data.remainingAmount);
}
if (data.commissionFixedAmount != null) {
commissionMode.value = 'amount';
commissionFixedAmount.value = String(data.commissionFixedAmount);
commissionPercent.value = String(
data.commissionPercent ?? defaultCommissionPercent.value,
);
} else {
commissionMode.value = 'percent';
commissionPercent.value = String(
data.commissionPercent ?? defaultCommissionPercent.value,
);
}
}
if (!lines.value.length) lines.value = [newLine()];
} else {
@@ -570,6 +616,18 @@ function invoicePayload(): CreateInvoiceRequest {
lineItems: toLineInputs(),
installments: toInstallmentInputs(),
currency: currency.value || 'IRR',
commissionPercent:
commissionMode.value === 'percent'
? commissionPercent.value === ''
? null
: Number(commissionPercent.value)
: null,
commissionFixedAmount:
commissionMode.value === 'amount'
? commissionFixedAmount.value === ''
? null
: Number(commissionFixedAmount.value)
: null,
};
}
@@ -671,6 +729,10 @@ async function submitPayment() {
error.value = 'مبلغ پرداخت را وارد کنید.';
return;
}
if (payMethod.value === 'check' && (!chequeNumber.value.trim() || !chequeBank.value.trim() || !chequeDue.value)) {
error.value = 'شماره، بانک و سررسید چک را وارد کنید.';
return;
}
paying.value = true;
error.value = '';
try {
@@ -680,6 +742,15 @@ async function submitPayment() {
notes: payNotes.value || undefined,
paidAt: payDate.value ? new Date(payDate.value).toISOString() : undefined,
installmentId: payInstallmentId.value || null,
cheque:
payMethod.value === 'check'
? {
number: chequeNumber.value.trim(),
bank: chequeBank.value.trim(),
dueDate: new Date(chequeDue.value).toISOString(),
status: chequeStatus.value,
}
: undefined,
};
const { data } = await api.post<InvoiceDto>(
`/invoices/${id.value}/payments`,
@@ -688,6 +759,10 @@ async function submitPayment() {
invoice.value = data;
payNotes.value = '';
payInstallmentId.value = '';
chequeNumber.value = '';
chequeBank.value = '';
chequeDue.value = '';
chequeStatus.value = 'pending';
await load();
} catch (e) {
error.value = axiosMessage(e, 'ثبت پرداخت ناموفق بود');
@@ -1128,6 +1203,45 @@ onMounted(load);
<span>مبلغ قابل پرداخت</span>
<strong>{{ formatMoney(payableAmount) }}</strong>
</div>
<div v-if="docMode === 'invoice'" class="commission-box">
<div class="days-row">
<button
type="button"
:class="{ on: commissionMode === 'percent' }"
@click="commissionMode = 'percent'"
>
درصد
</button>
<button
type="button"
:class="{ on: commissionMode === 'amount' }"
@click="commissionMode = 'amount'"
>
مبلغ ثابت
</button>
</div>
<label v-if="commissionMode === 'percent'" class="field">
<span>پورسانت کارشناس (%)</span>
<input
v-model="commissionPercent"
type="number"
min="0"
max="100"
step="0.1"
:placeholder="String(defaultCommissionPercent || 0)"
/>
</label>
<label v-else class="field">
<span>مبلغ پورسانت</span>
<MoneyInput v-model="commissionFixedAmount" :decimals="0" />
</label>
<p class="muted-sm">
برآورد پورسانت: {{ formatMoney(commissionPreview) }}
<template v-if="invoice?.commission?.status === 'posted'">
· ثبت‌شده {{ formatMoney(invoice.commission.amount) }}
</template>
</p>
</div>
</div>
</section>
@@ -1210,6 +1324,16 @@ onMounted(load);
</option>
</select>
<input v-model="payNotes" placeholder="یادداشت پرداخت" />
<div v-if="payMethod === 'check'" class="cheque-fields">
<input v-model="chequeNumber" placeholder="شماره چک" />
<input v-model="chequeBank" placeholder="بانک" />
<JalaliDateInput v-model="chequeDue" placeholder="سررسید چک" />
<select v-model="chequeStatus">
<option value="pending">در جریان</option>
<option value="cleared">وصول شد</option>
<option value="bounced">برگشت</option>
</select>
</div>
<button
class="btn primary"
type="button"
@@ -1230,6 +1354,9 @@ onMounted(load);
<strong>{{ formatMoney(p.amount) }}</strong>
<span class="muted">
· {{ methodLabel[p.method as PaymentMethod] || p.method }}
<template v-if="p.cheque">
· چک {{ p.cheque.number }} / {{ p.cheque.bank }}
</template>
· {{ p.paidAt.slice(0, 10) }}
</span>
</div>
@@ -1547,6 +1674,19 @@ onMounted(load);
<span>مبلغ قابل پرداخت</span>
<strong>{{ formatMoney(payableAmount) }}</strong>
</div>
<div v-if="docMode === 'invoice'" class="commission-box">
<label class="field">
<span>پورسانت کارشناس (%)</span>
<input
v-model="commissionPercent"
type="number"
min="0"
max="100"
step="0.1"
/>
</label>
<p class="muted-sm">برآورد: {{ formatMoney(commissionPreview) }}</p>
</div>
</div>
</section>
@@ -2366,6 +2506,19 @@ h1 {
gap: 0.5rem;
}
.cheque-fields,
.commission-box {
grid-column: 1 / -1;
display: grid;
gap: 0.45rem;
}
.commission-box {
margin-top: 0.45rem;
padding-top: 0.55rem;
border-top: 1px dashed var(--line);
}
.pay-list {
display: grid;
gap: 0.45rem;