Add salesperson commissions on issued invoices and a cheque register.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user