Add salesperson commissions on issued invoices and a cheque register.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import { api } from '@/lib/api';
|
||||
import { formatMoney } from '@/lib/money';
|
||||
import { formatJalaliNumeric } from '@/lib/jalali';
|
||||
import UiButton from '@/components/ui/UiButton.vue';
|
||||
import type { ChequeDto, ChequeStatus } from '@mento/shared';
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
const items = ref<ChequeDto[]>([]);
|
||||
const status = ref('');
|
||||
const q = ref('');
|
||||
const savingId = ref('');
|
||||
|
||||
const statusLabel: Record<ChequeStatus, string> = {
|
||||
pending: 'در جریان',
|
||||
cleared: 'وصول شد',
|
||||
bounced: 'برگشت',
|
||||
};
|
||||
|
||||
const filtered = computed(() => items.value);
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const { data } = await api.get<ChequeDto[]>('/cheques', {
|
||||
params: { status: status.value || undefined, q: q.value || undefined },
|
||||
});
|
||||
items.value = data;
|
||||
} catch (e) {
|
||||
error.value = axios.isAxiosError(e)
|
||||
? 'بارگذاری چکها ناموفق بود.'
|
||||
: 'بارگذاری چکها ناموفق بود.';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function setStatus(row: ChequeDto, next: ChequeStatus) {
|
||||
if (row.status === next) return;
|
||||
savingId.value = row.id;
|
||||
try {
|
||||
const { data } = await api.patch<ChequeDto>(`/cheques/${row.id}`, {
|
||||
status: next,
|
||||
});
|
||||
items.value = items.value.map((item) => (item.id === row.id ? data : item));
|
||||
} catch {
|
||||
error.value = 'تغییر وضعیت چک ناموفق بود.';
|
||||
} finally {
|
||||
savingId.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page" dir="rtl">
|
||||
<header class="head">
|
||||
<div>
|
||||
<h1>چکها</h1>
|
||||
<p class="sub">دفتر چکهای دریافتی وصل به فاکتور؛ وضعیت فقط برای پیگیری است</p>
|
||||
</div>
|
||||
<UiButton variant="ghost" type="button" :disabled="loading" @click="load">
|
||||
بروزرسانی
|
||||
</UiButton>
|
||||
</header>
|
||||
|
||||
<div class="toolbar">
|
||||
<input v-model="q" type="search" placeholder="شماره چک، بانک یا فاکتور" @keyup.enter="load" />
|
||||
<select v-model="status" @change="load">
|
||||
<option value="">همه وضعیتها</option>
|
||||
<option value="pending">در جریان</option>
|
||||
<option value="cleared">وصول شد</option>
|
||||
<option value="bounced">برگشت</option>
|
||||
</select>
|
||||
<UiButton type="button" @click="load">اعمال</UiButton>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-else-if="loading && !items.length" class="muted">در حال بارگذاری…</p>
|
||||
<p v-else-if="!filtered.length" class="muted">چکی ثبت نشده.</p>
|
||||
<div v-else class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>شماره</th>
|
||||
<th>بانک</th>
|
||||
<th>سررسید</th>
|
||||
<th>مبلغ</th>
|
||||
<th>فاکتور</th>
|
||||
<th>طرفحساب</th>
|
||||
<th>وضعیت</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in filtered" :key="row.id">
|
||||
<td>{{ row.number }}</td>
|
||||
<td>{{ row.bank }}</td>
|
||||
<td>{{ formatJalaliNumeric(new Date(row.dueDate)) }}</td>
|
||||
<td>{{ formatMoney(row.amount, row.currency) }}</td>
|
||||
<td>
|
||||
<RouterLink :to="{ name: 'invoice-detail', params: { id: row.invoiceId } }">
|
||||
{{ row.invoiceNumber }}
|
||||
</RouterLink>
|
||||
</td>
|
||||
<td>{{ row.personName || '—' }}</td>
|
||||
<td>
|
||||
<select
|
||||
:value="row.status"
|
||||
:disabled="savingId === row.id"
|
||||
@change="setStatus(row, ($event.target as HTMLSelectElement).value as ChequeStatus)"
|
||||
>
|
||||
<option value="pending">{{ statusLabel.pending }}</option>
|
||||
<option value="cleared">{{ statusLabel.cleared }}</option>
|
||||
<option value="bounced">{{ statusLabel.bounced }}</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page { display: grid; gap: 1rem; }
|
||||
.head { display: flex; justify-content: space-between; gap: 1rem; align-items: flex-start; }
|
||||
h1 { margin: 0; font-size: 1.25rem; }
|
||||
.sub, .muted { margin: 0.25rem 0 0; color: var(--muted); font-size: 0.88rem; }
|
||||
.error { color: var(--danger, #dc2626); font-weight: 600; }
|
||||
.toolbar {
|
||||
display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;
|
||||
}
|
||||
.toolbar input, .toolbar select {
|
||||
min-height: var(--control-h); border: 1px solid var(--line); border-radius: var(--radius);
|
||||
padding: 0 0.75rem; background: #fff;
|
||||
}
|
||||
.table-wrap { overflow: auto; background: #fff; border: 1px solid var(--line); border-radius: var(--radius); }
|
||||
table { width: 100%; border-collapse: collapse; min-width: 720px; }
|
||||
th, td { padding: 0.7rem 0.65rem; border-bottom: 1px solid #eef2f7; text-align: right; }
|
||||
th { background: #f8fafc; color: #64748b; font-size: 0.82rem; }
|
||||
select {
|
||||
min-height: var(--control-h); border: 1px solid var(--line); border-radius: var(--radius);
|
||||
padding: 0 0.55rem; background: #fff;
|
||||
}
|
||||
a { color: #0f766e; font-weight: 700; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user