A receipt or payment only reduces the party balance, while an income or expense keeps both amounts. Finance screens stack into cards on a phone so the forms and lists stay readable. Co-authored-by: Cursor <cursoragent@cursor.com>
81 lines
2.8 KiB
Vue
81 lines
2.8 KiB
Vue
<script setup lang="ts">
|
|
import { computed, nextTick, ref, watch } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { Segmented as ASegmented } from 'ant-design-vue';
|
|
import { treasurySectionAllowed, type TreasurySectionKey } from '@mento/shared';
|
|
import { useAuthStore } from '@/stores/auth';
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const auth = useAuthStore();
|
|
const scroller = ref<HTMLElement | null>(null);
|
|
|
|
const links = computed(() => {
|
|
const role = auth.user?.role;
|
|
const permissions = auth.user?.permissions;
|
|
const all: Array<{ value: string; label: string; exact?: boolean; section: TreasurySectionKey }> = [
|
|
{ value: '/finance', label: 'داشبورد', exact: true, section: 'treasury_dashboard' },
|
|
{ value: '/finance/incomes', label: 'دریافتها و درآمدها', section: 'treasury_incomes' },
|
|
{ value: '/finance/expenses', label: 'پرداختها و هزینهها', section: 'treasury_expenses' },
|
|
{ value: '/finance/receivables', label: 'اقساط و مطالبات', section: 'treasury_receivables' },
|
|
{ value: '/finance/accounts', label: 'صندوق و حسابها', section: 'treasury_accounts' },
|
|
{ value: '/finance/payroll', label: 'حقوق', section: 'treasury_payroll' },
|
|
{ value: '/finance/recurring', label: 'تکرارشونده', section: 'treasury_recurring' },
|
|
{ value: '/finance/reports', label: 'گزارش', section: 'treasury_reports' },
|
|
{ value: '/finance/audit', label: 'سوابق', section: 'treasury_audit' },
|
|
{ value: '/finance/categories', label: 'دستهبندی', section: 'treasury_categories' },
|
|
];
|
|
return all.filter((link) => treasurySectionAllowed(role, permissions, link.section));
|
|
});
|
|
|
|
const options = computed(() => links.value.map((l) => ({ value: l.value, label: l.label })));
|
|
|
|
const active = computed(() => {
|
|
const path = route.path;
|
|
for (const link of links.value) {
|
|
if (link.exact) {
|
|
if (path === link.value) return link.value;
|
|
} else if (path === link.value || path.startsWith(`${link.value}/`)) {
|
|
return link.value;
|
|
}
|
|
}
|
|
return '/finance';
|
|
});
|
|
|
|
watch(
|
|
active,
|
|
async () => {
|
|
await nextTick();
|
|
scroller.value
|
|
?.querySelector('.ant-segmented-item-selected')
|
|
?.scrollIntoView({ inline: 'center', block: 'nearest' });
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
function onChange(value: string | number) {
|
|
const next = String(value);
|
|
if (next && next !== route.path) void router.push(next);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
ref="scroller"
|
|
:style="{
|
|
width: '100%',
|
|
overflowX: 'auto',
|
|
flexShrink: 0,
|
|
minHeight: '36px',
|
|
}"
|
|
>
|
|
<a-segmented
|
|
:value="active"
|
|
:options="options"
|
|
dir="rtl"
|
|
:style="{ minWidth: 'max-content' }"
|
|
@change="onChange"
|
|
/>
|
|
</div>
|
|
</template>
|