113 lines
4.4 KiB
Vue
113 lines
4.4 KiB
Vue
<script setup lang="ts">
|
|||
|
|
import { onMounted, ref } from 'vue';
|
||
|
|
import { api } from '@/lib/api';
|
||
|
|
import type { TreasuryDashboardDto } from '@mento/shared';
|
||
|
|
import { UiCard, UiPageHeader } from '@/components/ui';
|
||
|
|
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
|
||
|
|
import { toman } from './treasury-format';
|
||
|
|
|
||
|
|
const data = ref<TreasuryDashboardDto | null>(null);
|
||
|
|
const error = ref('');
|
||
|
|
|
||
|
|
const cards = [
|
||
|
|
{ key: 'bookedIncome', label: 'درآمد ثبتشده', hint: 'تعهدی' },
|
||
|
|
{ key: 'actualReceipts', label: 'دریافت واقعی', hint: 'ورود به حساب' },
|
||
|
|
{ key: 'bookedExpense', label: 'هزینه ثبتشده', hint: 'تعهدی' },
|
||
|
|
{ key: 'actualPayments', label: 'پرداخت واقعی', hint: 'خروج از حساب' },
|
||
|
|
{ key: 'operatingProfit', label: 'سود یا زیان عملیاتی', hint: 'درآمد ثبتشده − هزینه ثبتشده' },
|
||
|
|
{ key: 'receivables', label: 'مطالبات وصولنشده', hint: 'درآمد بدون دریافت کامل' },
|
||
|
|
{ key: 'payables', label: 'تعهدات پرداخت', hint: 'هزینه بدون پرداخت کامل' },
|
||
|
|
{ key: 'totalBalance', label: 'موجودی حسابها', hint: 'صندوق و بانک' },
|
||
|
|
] as const;
|
||
|
|
|
||
|
|
onMounted(async () => {
|
||
|
|
try {
|
||
|
|
const res = await api.get<TreasuryDashboardDto>('/treasury/dashboard');
|
||
|
|
data.value = res.data;
|
||
|
|
} catch {
|
||
|
|
error.value = 'بارگذاری داشبورد مالی ناموفق بود.';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<template>
|
||
|
|
<section class="page">
|
||
|
|
<UiPageHeader
|
||
|
|
title="مدیریت مالی"
|
||
|
|
description="دفتر داخلی درآمد، هزینه و موجودی حساب. این بخش جایگزین فاکتور فروش یا حسابداری رسمی نیست."
|
||
|
|
/>
|
||
|
|
<TreasuryNav />
|
||
|
|
<p v-if="error" class="err">{{ error }}</p>
|
||
|
|
<div v-if="data" class="stats">
|
||
|
|
<article v-for="card in cards" :key="card.key">
|
||
|
|
<span>{{ card.label }}</span>
|
||
|
|
<strong>{{ toman(data[card.key]) }}</strong>
|
||
|
|
<small>{{ card.hint }}</small>
|
||
|
|
</article>
|
||
|
|
</div>
|
||
|
|
<div v-if="data" class="split">
|
||
|
|
<UiCard title="اقساط نزدیک یا معوق">
|
||
|
|
<p v-if="!data.upcomingInstallments.length" class="muted">قسط بازی نیست.</p>
|
||
|
|
<ul v-else class="list">
|
||
|
|
<li v-for="row in data.upcomingInstallments" :key="row.id">
|
||
|
|
<div>
|
||
|
|
<strong>قسط {{ row.position }}</strong>
|
||
|
|
<small>{{ row.status === 'overdue' ? 'معوق' : 'باز' }} · مانده {{ toman(row.remaining) }}</small>
|
||
|
|
</div>
|
||
|
|
<b>{{ toman(row.amount) }}</b>
|
||
|
|
</li>
|
||
|
|
</ul>
|
||
|
|
</UiCard>
|
||
|
|
<UiCard title="پرداختهای آینده">
|
||
|
|
<p v-if="!data.upcomingPayables.length" class="muted">تعهد پرداخت بازی نیست.</p>
|
||
|
|
<ul v-else class="list">
|
||
|
|
<li v-for="row in data.upcomingPayables" :key="row.id">
|
||
|
|
<div>
|
||
|
|
<strong>{{ row.title }}</strong>
|
||
|
|
<small>مانده {{ toman(row.remaining) }}</small>
|
||
|
|
</div>
|
||
|
|
<b>{{ toman(row.amount) }}</b>
|
||
|
|
</li>
|
||
|
|
</ul>
|
||
|
|
</UiCard>
|
||
|
|
</div>
|
||
|
|
<UiCard v-if="data" title="آخرین اسناد">
|
||
|
|
<p v-if="!data.recent.length" class="muted">هنوز سندی ثبت نشده است.</p>
|
||
|
|
<ul v-else class="list">
|
||
|
|
<li v-for="row in data.recent" :key="row.id">
|
||
|
|
<div>
|
||
|
|
<strong>{{ row.title }}</strong>
|
||
|
|
<small>{{ row.kind === 'income' ? 'درآمد' : 'هزینه' }} · {{ row.status }}</small>
|
||
|
|
</div>
|
||
|
|
<b>{{ toman(row.amount) }}</b>
|
||
|
|
</li>
|
||
|
|
</ul>
|
||
|
|
</UiCard>
|
||
|
|
</section>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.page { display: grid; gap: 1rem; }
|
||
|
|
.stats {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||
|
|
gap: 0.75rem;
|
||
|
|
}
|
||
|
|
.stats article {
|
||
|
|
background: #fff;
|
||
|
|
border: 1px solid #e7eef2;
|
||
|
|
border-radius: 16px;
|
||
|
|
padding: 0.9rem 1rem;
|
||
|
|
display: grid;
|
||
|
|
gap: 0.25rem;
|
||
|
|
}
|
||
|
|
.stats span, .stats small, .muted { color: #64748b; }
|
||
|
|
.stats strong { font-size: 1.05rem; }
|
||
|
|
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
|
||
|
|
.list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.55rem; }
|
||
|
|
@media (max-width: 800px) { .split { grid-template-columns: 1fr; } }
|
||
|
|
.list li { display: flex; justify-content: space-between; gap: 0.75rem; }
|
||
|
|
.list small { display: block; color: #64748b; }
|
||
|
|
.err { color: #b91c1c; font-weight: 700; }
|
||
|
|
</style>
|