feat(treasury): separate receipts from income and fit finance on a phone
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>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
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';
|
||||
@@ -8,6 +8,7 @@ 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;
|
||||
@@ -41,6 +42,17 @@ const active = computed(() => {
|
||||
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);
|
||||
@@ -49,6 +61,7 @@ function onChange(value: string | number) {
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="scroller"
|
||||
:style="{
|
||||
width: '100%',
|
||||
overflowX: 'auto',
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { Table as ATable } from 'ant-design-vue';
|
||||
import {
|
||||
Flex as AFlex,
|
||||
List as AList,
|
||||
Table as ATable,
|
||||
Typography,
|
||||
} from 'ant-design-vue';
|
||||
import type { TreasuryPartyBalanceDto } from '@mento/shared';
|
||||
import { toman } from '@/views/treasury/treasury-format';
|
||||
import { useTreasuryMobile } from '@/views/treasury/useTreasuryMobile';
|
||||
|
||||
const props = defineProps<{
|
||||
rows: TreasuryPartyBalanceDto[];
|
||||
settledLabel: string;
|
||||
}>();
|
||||
|
||||
const isMobile = useTreasuryMobile();
|
||||
const { Text: ATypographyText } = Typography;
|
||||
const AListItem = AList.Item;
|
||||
|
||||
const kindLabel: Record<string, string> = {
|
||||
person: 'مشتری',
|
||||
company: 'شرکت',
|
||||
@@ -39,10 +49,43 @@ const columns = computed(() => [
|
||||
customRender: ({ record }: { record: TreasuryPartyBalanceDto }) => toman(record.remaining),
|
||||
},
|
||||
]);
|
||||
|
||||
function kindOf(record: TreasuryPartyBalanceDto) {
|
||||
return (record.partyKind && kindLabel[record.partyKind]) || 'ثبت قبلی';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-list
|
||||
v-if="isMobile"
|
||||
:data-source="rows"
|
||||
:locale="{ emptyText: 'ماندهای نیست.' }"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-flex vertical :gap="4" style="width: 100%">
|
||||
<a-flex justify="space-between" align="center" wrap="wrap" :gap="8">
|
||||
<a-typography-text strong>{{ item.partyName }}</a-typography-text>
|
||||
<a-typography-text type="secondary">{{ kindOf(item) }}</a-typography-text>
|
||||
</a-flex>
|
||||
<a-flex justify="space-between" align="center" :gap="8">
|
||||
<a-typography-text type="secondary">مبلغ کل</a-typography-text>
|
||||
<a-typography-text>{{ toman(item.amount) }}</a-typography-text>
|
||||
</a-flex>
|
||||
<a-flex justify="space-between" align="center" :gap="8">
|
||||
<a-typography-text type="secondary">{{ settledLabel }}</a-typography-text>
|
||||
<a-typography-text>{{ toman(item.settledAmount) }}</a-typography-text>
|
||||
</a-flex>
|
||||
<a-flex justify="space-between" align="center" :gap="8">
|
||||
<a-typography-text type="secondary">مانده</a-typography-text>
|
||||
<a-typography-text strong>{{ toman(item.remaining) }}</a-typography-text>
|
||||
</a-flex>
|
||||
</a-flex>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
<a-table
|
||||
v-else
|
||||
:columns="columns"
|
||||
:data-source="rows"
|
||||
:pagination="false"
|
||||
|
||||
@@ -25,6 +25,11 @@ const ASelectOption = ASelect.Option;
|
||||
const ASelectOptGroup = ASelect.OptGroup;
|
||||
const { Text: ATypographyText } = Typography;
|
||||
|
||||
withDefaults(
|
||||
defineProps<{ allowOther?: boolean; placeholder?: string }>(),
|
||||
{ allowOther: true, placeholder: 'مشتری، شرکت، کاربر یا سایر' },
|
||||
);
|
||||
|
||||
const kind = defineModel<FinancialPartyKind | undefined>('kind');
|
||||
const partyId = defineModel<string | undefined>('partyId');
|
||||
|
||||
@@ -156,12 +161,12 @@ onMounted(() => {
|
||||
:filter-option="false"
|
||||
:loading="loading"
|
||||
:value="selectValue"
|
||||
placeholder="مشتری، شرکت، کاربر یا سایر"
|
||||
:placeholder="placeholder"
|
||||
style="width: 100%"
|
||||
@search="onSearch"
|
||||
@change="onChange"
|
||||
>
|
||||
<a-select-option value="other" label="سایر">سایر</a-select-option>
|
||||
<a-select-option v-if="allowOther" value="other" label="سایر">سایر</a-select-option>
|
||||
<a-select-opt-group v-if="peopleOptions.length" label="مشتریان">
|
||||
<a-select-option
|
||||
v-for="row in peopleOptions"
|
||||
|
||||
Reference in New Issue
Block a user