hesabixCore/webUI/src/views/acc/persons/card.vue

639 lines
26 KiB
Vue
Raw Normal View History

2025-03-21 14:20:43 +03:30
<template>
<!-- Toolbar بالای صفحه -->
<v-toolbar color="toolbar" dense flat>
<v-btn icon @click="$router.back()" class="d-none d-md-flex">
<v-icon>mdi-arrow-right</v-icon>
</v-btn>
<v-toolbar-title class="text-primary-dark">
{{ $t('pages.person_card.title') }}
</v-toolbar-title>
<v-spacer />
<v-btn color="primary" size="small" @click="dialog = true" :loading="loading" prepend-icon="mdi-bank">
{{ $t('dialog.banks_accounts') }}
</v-btn>
2025-08-11 15:20:24 +03:30
<v-btn color="warning" size="small" @click="editPersonDialog = true" :loading="loading" prepend-icon="mdi-account-edit" class="ml-2">
ویرایش شخص
</v-btn>
2025-03-21 14:20:43 +03:30
<v-menu>
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="" color="red">
<v-tooltip activator="parent" :text="$t('dialog.export_pdf')" location="bottom" />
<v-icon icon="mdi-file-pdf-box"></v-icon>
</v-btn>
</template>
<v-list>
<v-list-subheader color="primary">{{ $t('dialog.export_pdf') }}</v-list-subheader>
<v-list-item class="text-dark" :title="$t('dialog.selected')" @click="print(false)">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-check"></v-icon>
</template>
</v-list-item>
<v-list-item class="text-dark" :title="$t('dialog.selected_all')" @click="print(true)">
<template v-slot:prepend>
<v-icon color="indigo-darken-4" icon="mdi-expand-all"></v-icon>
</template>
</v-list-item>
</v-list>
</v-menu>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="" color="green">
<v-tooltip activator="parent" :text="$t('dialog.export_excel')" location="bottom" />
<v-icon icon="mdi-file-excel-box"></v-icon>
</v-btn>
</template>
<v-list>
<v-list-subheader color="primary">{{ $t('dialog.export_excel') }}</v-list-subheader>
<v-list-item class="text-dark" :title="$t('dialog.selected')" @click="excellOutput(false)">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-check"></v-icon>
</template>
</v-list-item>
<v-list-item class="text-dark" :title="$t('dialog.selected_all')" @click="excellOutput(true)">
<template v-slot:prepend>
<v-icon color="indigo-darken-4" icon="mdi-expand-all"></v-icon>
</template>
</v-list-item>
</v-list>
</v-menu>
</v-toolbar>
<!-- دیالوگ حسابهای بانکی -->
<v-dialog v-model="dialog" max-width="500" persistent>
<v-card>
<v-toolbar color="primary-dark" dense flat>
<v-toolbar-title class="text-white">{{ $t('dialog.banks_accounts') }}</v-toolbar-title>
<v-spacer />
<v-btn icon @click="dialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar>
<v-card-text class="pa-2">
<template v-if="selectedPerson.accounts && selectedPerson.accounts.length > 0">
<v-list dense>
<v-list-item v-for="item in selectedPerson.accounts" :key="item.accountNum" class="pa-1">
<template v-slot:title>
<span class="bg-primary-dark text-white pa-1 rounded d-block">
{{ item.bank }}: {{ item.accountNum }}
</span>
</template>
<template v-slot:subtitle>
<div>{{ $t('pages.person.card_number') }}: {{ item.cardNum }}</div>
<div>{{ $t('pages.person.shaba_number') }}: {{ item.shabaNum }}</div>
</template>
</v-list-item>
</v-list>
</template>
<v-alert v-else type="error" dense text class="ma-0">
{{ $t('pages.person_card.no_bank_accounts') }}
</v-alert>
</v-card-text>
<v-card-actions>
<v-btn color="secondary" text @click="dialog = false">{{ $t('dialog.cancel') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
2025-08-11 15:20:24 +03:30
<!-- دیالوگ ویرایش شخص -->
<v-dialog v-model="editPersonDialog" max-width="600" persistent>
<v-card>
<v-toolbar color="primary-dark" dense flat>
<v-toolbar-title class="text-white">ویرایش شخص</v-toolbar-title>
<v-spacer />
<v-btn icon @click="editPersonDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar>
<v-card-text class="pa-4">
<v-form ref="editPersonForm" v-model="editPersonFormValid">
<v-row dense>
<v-col cols="12" md="6">
<v-text-field
v-model="editPersonData.nikename"
label="نام مستعار"
dense
required
:rules="[v => !!v || 'نام مستعار الزامی است']"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="editPersonData.name"
label="نام کامل"
dense
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="editPersonData.mobile"
label="موبایل"
dense
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="editPersonData.tel"
label="تلفن"
dense
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="editPersonData.address"
label="آدرس"
dense
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="editPersonData.des"
label="توضیحات"
dense
/>
</v-col>
<v-col cols="12">
<v-divider class="my-2" />
<v-card-subtitle class="px-0 text-primary">
تأیید دو مرحلهای
</v-card-subtitle>
<v-switch
v-model="editPersonData.requireTwoStep"
label="فاکتورها و حواله‌های این شخص نیاز به تأیید دو مرحله‌ای دارند"
color="warning"
hide-details
class="mt-2"
/>
<v-alert type="info" variant="tonal" dense class="mt-2">
اگر این گزینه فعال باشد، تمام فاکتورها، حوالههای انبار و اسناد مالی مرتبط با این شخص نیاز به تأیید دو مرحلهای خواهند داشت.
</v-alert>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="secondary" text @click="editPersonDialog = false">
انصراف
</v-btn>
<v-btn color="primary" @click="savePersonChanges" :loading="saveLoading" :disabled="!editPersonFormValid">
ذخیره
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
2025-03-21 14:20:43 +03:30
<!-- محتوای اصلی -->
<v-container fluid class="pa-4">
2025-03-21 14:20:43 +03:30
<v-row dense>
<v-col cols="12" md="12">
<v-autocomplete v-model="selectedPerson" :items="listPersons" item-title="nikename" item-value="code"
return-object :label="$t('dialog.user_info')" dense hide-details prepend-inner-icon="mdi-account"
:loading="loading" @update:search="debouncedSearchPerson" @update:model-value="updateRoute"
class="rounded-lg elevation-2">
2025-03-21 14:20:43 +03:30
<template v-slot:no-data>
{{ $t('pages.person_card.no_results') }}
</template>
<template v-slot:item="{ props, item }">
<v-list-item v-bind="props">
<v-list-item-title>
<v-icon small left>mdi-account</v-icon>
{{ item.raw.nikename }}
</v-list-item-title>
<v-list-item-subtitle>
<v-row dense>
<v-col cols="6">
<v-icon small left>mdi-phone</v-icon>
{{ item.raw.mobile }}
</v-col>
<v-col cols="6">
<v-icon small left>mdi-scale-balance</v-icon>
{{ $t('pages.person_card.balance') }}: {{ $filters.formatNumber(Math.abs(parseInt(item.raw.bs) -
parseInt(item.raw.bd))) }}
<span :class="parseInt(item.raw.bs) - parseInt(item.raw.bd) < 0 ? 'text-danger' : 'text-success'">
{{ parseInt(item.raw.bs) - parseInt(item.raw.bd) < 0 ? $t('pages.person_card.debtor') :
$t('pages.person_card.creditor') }} </span>
</v-col>
</v-row>
</v-list-item-subtitle>
</v-list-item>
</template>
</v-autocomplete>
</v-col>
<v-col cols="12" md="6">
<v-card flat outlined class="rounded-lg elevation-2">
<v-toolbar color="primary-dark" dense flat class="rounded-t-lg">
2025-03-21 14:20:43 +03:30
<v-toolbar-title class="text-white">
{{ $t('pages.person_card.account_card') }}
<small class="text-info-light" v-if="selectedPerson">{{ selectedPerson.nikename }}</small>
</v-toolbar-title>
</v-toolbar>
<v-card-text class="pa-2">
<div class="text-subtitle-2">{{ $t('pages.person_card.accounting_code') }}: <span class="text-primary">{{
selectedPerson.code || '-' }}</span></div>
<div class="text-subtitle-2">{{ $t('pages.person.nickname') }}: <span class="text-primary">{{
selectedPerson.nikename || '-' }}</span></div>
<div class="text-subtitle-2">{{ $t('pages.person.name') }}: <span class="text-primary">{{
selectedPerson.name || '-' }}</span></div>
<div class="text-subtitle-2">{{ $t('pages.person.phone') }}: <span class="text-primary">{{
selectedPerson.tel || '-' }}</span></div>
<div class="text-subtitle-2">{{ $t('pages.person.mobile') }}: <span class="text-primary">{{
selectedPerson.mobile || '-' }}</span></div>
<div class="text-subtitle-2">{{ $t('pages.person.address') }}: <span class="text-primary">{{
selectedPerson.address || '-' }}</span></div>
<div class="text-subtitle-2">{{ $t('pages.person.description') }}: <span class="text-primary">{{
selectedPerson.des || '-' }}</span></div>
2025-08-11 15:20:24 +03:30
<!-- <v-divider class="my-3" />
<div class="text-subtitle-2 d-flex align-center justify-space-between">
<span>تأیید دو مرحلهای:</span>
<v-switch
v-model="selectedPerson.requireTwoStep"
color="warning"
hide-details
density="compact"
@change="updateTwoStepApproval"
:loading="twoStepUpdateLoading"
/>
</div>
<div class="text-caption text-grey mt-1">
{{ selectedPerson.requireTwoStep ? 'فعال - فاکتورها و حواله‌ها نیاز به تأیید دارند' : 'غیرفعال - طبق قوانین عادی' }}
</div> -->
2025-03-21 14:20:43 +03:30
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card flat outlined class="rounded-lg elevation-2">
<v-toolbar color="primary-dark" dense flat class="rounded-t-lg">
2025-03-21 14:20:43 +03:30
<v-toolbar-title class="text-white">
{{ $t('pages.person_card.account_status') }}
<small class="text-info-light" v-if="selectedPerson">{{ selectedPerson.nikename }}</small>
</v-toolbar-title>
</v-toolbar>
<v-card-text class="pa-2">
<div class="text-subtitle-2">
{{ $t('pages.person_card.accounting_status') }}:
<span :class="{
'text-success': selectedPerson.balance > 0,
'text-danger': selectedPerson.balance < 0,
'text-dark': selectedPerson.balance == 0
}">
{{ selectedPerson.balance > 0 ? $t('pages.person_card.creditor') : selectedPerson.balance < 0 ?
$t('pages.person_card.debtor') : $t('pages.person_card.settled') }} </span>
</div>
<div class="text-subtitle-2">{{ $t('pages.person_card.credit') }}: <span class="text-primary">{{
$filters.formatNumber(selectedPerson.bs) || '-' }}</span></div>
<div class="text-subtitle-2">{{ $t('pages.person_card.debit') }}: <span class="text-primary">{{
$filters.formatNumber(selectedPerson.bd) || '-' }}</span></div>
<div class="text-subtitle-2">{{ $t('pages.person_card.accounting_balance') }}: <span class="text-primary">{{
$filters.formatNumber(selectedPerson.balance) || '-' }}</span></div>
2025-08-11 15:20:24 +03:30
<v-divider class="my-2" />
<div class="text-subtitle-2">
تأیید دو مرحلهای:
<v-chip :color="selectedPerson.requireTwoStep ? 'warning' : 'success'" size="small" class="ml-2">
{{ selectedPerson.requireTwoStep ? 'فعال' : 'غیرفعال' }}
</v-chip>
</div>
2025-03-21 14:20:43 +03:30
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- جدول تراکنشها -->
<v-row dense>
<v-col cols="12">
<v-data-table v-model="itemsSelected" :headers="headers" :items="items" :search="searchValue" :loading="loading"
show-select dense :items-per-page="25" class="elevation-2 rounded-lg" :header-props="{ class: 'custom-header' }">
2025-03-21 14:20:43 +03:30
<template v-slot:top>
<v-toolbar flat dense color="grey-lighten-4" class="rounded-t-lg">
2025-03-21 14:20:43 +03:30
<v-toolbar-title class="text-subtitle-1">{{ $t('pages.person_card.transactions') }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-text-field v-model="searchValue" dense hide-details
prepend-inner-icon="mdi-magnify" />
</v-toolbar>
</template>
<template v-slot:item.operation="{ item }">
<v-btn variant="plain" icon size="small" :to="'/acc/accounting/view/' + item.code" color="success">
<v-icon small>mdi-eye</v-icon>
</v-btn>
</template>
<template v-slot:item.code="{ item }">
{{ $filters.formatNumber(item.code) }}
</template>
<template v-slot:item.type="{ item }">
<v-btn variant="plain" text size="small" :to="getTypeRoute(item.type, item.code)" class="text-none">
{{ getTypeLabel(item.type) }}
</v-btn>
</template>
<template v-slot:item.bd="{ item }">
{{ $filters.formatNumber(item.bd) }}
</template>
<template v-slot:item.bs="{ item }">
{{ $filters.formatNumber(item.bs) }}
</template>
2025-08-18 11:03:23 +03:30
<template v-slot:item.settlement="{ item }">
<v-chip
:color="item.settlement === 'بستانکار' ? 'success' : item.settlement === 'بدهکار' ? 'error' : item.settlement === 'تسویه‌شده' ? 'info' : 'default'"
size="small"
variant="outlined"
>
{{ item.settlement }}
</v-chip>
</template>
<template v-slot:item.balance="{ item }">
<span :class="{
'text-success': item.balance > 0,
'text-danger': item.balance < 0,
'text-dark': item.balance == 0
}">
{{ $filters.formatNumber(item.balance) }}
</span>
</template>
2025-03-21 14:20:43 +03:30
<template v-slot:no-data>
{{ $t('pages.person_card.no_data') }}
</template>
</v-data-table>
</v-col>
</v-row>
</v-container>
<v-overlay :value="loading" contained class="align-center justify-center">
<v-progress-circular indeterminate size="64" />
</v-overlay>
</template>
<script>
import axios from "axios";
import Swal from "sweetalert2";
import { ref } from "vue";
export default {
name: "card",
data() {
return {
searchValue: '',
listPersons: [],
itemsSelected: [],
2025-08-11 15:20:24 +03:30
selectedPerson: { accounts: [], balance: 0, bs: 0, bd: 0, requireTwoStep: false },
2025-03-21 14:20:43 +03:30
items: [],
loading: ref(false),
dialog: false,
2025-08-11 15:20:24 +03:30
editPersonDialog: false,
editPersonFormValid: false,
saveLoading: false,
editPersonData: {
nikename: '',
name: '',
mobile: '',
tel: '',
address: '',
des: '',
requireTwoStep: false
},
2025-03-21 14:20:43 +03:30
debounceTimeout: null, // برای مدیریت debounce
headers: [
{ title: this.$t('dialog.operation'), key: "operation", align: "center", sortable: false },
{ title: this.$t('dialog.type'), key: "type", align: "center", sortable: true },
{ title: this.$t('dialog.invoice_num'), key: "code", align: "center", sortable: true },
{ title: this.$t('dialog.date'), key: "date", align: "center", sortable: true },
{ title: this.$t('app.body'), key: "des", align: "center" },
{ title: this.$t('pages.person_card.detail'), key: "ref", align: "center", sortable: true },
{ title: this.$t('pages.person_card.debit'), key: "bd", align: "center", sortable: true },
{ title: this.$t('pages.person_card.credit'), key: "bs", align: "center", sortable: true },
2025-08-18 11:03:23 +03:30
{ title: this.$t('pages.person_card.settlement'), key: "settlement", align: "center", sortable: true },
{ title: this.$t('pages.person_card.running_balance'), key: "balance", align: "center", sortable: true },
2025-03-21 14:20:43 +03:30
],
};
},
mounted() {
this.loadData();
},
methods: {
debouncedSearchPerson(search) {
// لغو تایمر قبلی اگه وجود داشته باشه
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout);
}
// تنظیم تایمر جدید برای ۱ ثانیه
this.debounceTimeout = setTimeout(() => {
this.searchPerson(search);
}, 1000);
},
async searchPerson(search) {
if (!search || search.length < 1) {
this.listPersons = [];
return;
}
this.loading = true;
try {
const response = await axios.post('/api/person/list/search', { search });
this.listPersons = response.data;
} catch (error) {
console.error('Search error:', error);
this.listPersons = [];
} finally {
this.loading = false;
}
},
updateRoute() {
if (this.selectedPerson && this.selectedPerson.code) {
this.$router.push(this.selectedPerson.code);
this.loadPerson(this.selectedPerson.code);
}
},
async loadData() {
this.loading = true;
try {
const response = await axios.post('/api/person/list/search');
this.listPersons = response.data;
const id = this.$route.params.id;
if (id) {
await this.loadPerson(id);
} else if (response.data.length > 0) {
this.selectedPerson = response.data[0];
await this.loadPerson(this.selectedPerson.code);
}
} catch (error) {
console.error('Load data error:', error);
} finally {
this.loading = false;
}
},
async loadPerson(id) {
this.loading = true;
try {
const personResponse = await axios.post('/api/person/info/' + id);
this.selectedPerson = personResponse.data;
2025-08-11 15:20:24 +03:30
// پر کردن فرم ویرایش با اطلاعات فعلی شخص
this.editPersonData = {
nikename: this.selectedPerson.nikename || '',
name: this.selectedPerson.name || '',
mobile: this.selectedPerson.mobile || '',
tel: this.selectedPerson.tel || '',
address: this.selectedPerson.address || '',
des: this.selectedPerson.des || '',
requireTwoStep: this.selectedPerson.requireTwoStep || false
};
2025-03-21 14:20:43 +03:30
const rowsResponse = await axios.post('/api/accounting/rows/search', { type: 'person', id });
this.items = rowsResponse.data;
} catch (error) {
console.error('Load person error:', error);
2025-08-11 15:20:24 +03:30
this.selectedPerson = { accounts: [], balance: 0, bs: 0, bd: 0, requireTwoStep: false };
2025-03-21 14:20:43 +03:30
this.items = [];
} finally {
this.loading = false;
}
},
2025-08-11 15:20:24 +03:30
async savePersonChanges() {
if (!this.selectedPerson || !this.selectedPerson.code) {
this.snackbar = { show: true, text: 'شخص انتخاب نشده است', color: 'error' };
return;
}
this.saveLoading = true;
try {
const response = await axios.post('/api/person/mod/' + this.selectedPerson.code, this.editPersonData);
if (response.data.Success) {
this.snackbar = { show: true, text: 'اطلاعات شخص با موفقیت بروزرسانی شد', color: 'success' };
this.editPersonDialog = false;
// بروزرسانی اطلاعات شخص
await this.loadPerson(this.selectedPerson.code);
} else {
this.snackbar = { show: true, text: 'خطا در بروزرسانی اطلاعات شخص', color: 'error' };
}
} catch (error) {
console.error('Save person error:', error);
this.snackbar = { show: true, text: 'خطا در بروزرسانی اطلاعات شخص', color: 'error' };
} finally {
this.saveLoading = false;
}
},
2025-03-21 14:20:43 +03:30
async excellOutput(allItems = true) {
if (!allItems && this.itemsSelected.length === 0) {
Swal.fire({ text: this.$t('pages.person_card.no_items_selected'), icon: 'info', confirmButtonText: this.$t('dialog.confirm') });
return;
}
try {
const response = await axios({
method: 'post',
url: '/api/person/card/list/excel',
data: allItems ? { code: this.selectedPerson.code } : { code: this.selectedPerson.code, items: this.itemsSelected },
responseType: 'arraybuffer',
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'person-card-view.xlsx');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (error) {
console.error('Excel output error:', error);
}
},
async print(allItems = true) {
if (!this.selectedPerson) {
Swal.fire({ text: this.$t('pages.person_card.no_items_selected'), icon: 'info', confirmButtonText: this.$t('dialog.confirm') });
return;
}
if (!allItems && this.itemsSelected.length === 0) {
Swal.fire({ text: this.$t('pages.person_card.no_items_selected'), icon: 'info', confirmButtonText: this.$t('dialog.confirm') });
return;
}
try {
const response = await axios.post('/api/person/card/list/print', allItems ? { code: this.selectedPerson.code } : { code: this.selectedPerson.code, items: this.itemsSelected });
window.open(this.$API_URL + '/front/print/' + response.data.id, '_blank', 'noreferrer');
} catch (error) {
console.error('Print error:', error);
}
},
getTypeRoute(type, code) {
const routes = {
sell: '/acc/sell/view/',
buy: '/acc/buy/view/',
rfbuy: '/acc/rfbuy/view/',
rfsell: '/acc/rfsell/view/',
person_send: '/acc/accounting/view/',
person_receive: '/acc/accounting/view/',
cost: '/acc/accounting/view/',
income: '/acc/accounting/view/',
sell_receive: '/acc/accounting/view/',
buy_send: '/acc/accounting/view/',
2025-04-12 18:50:34 +03:30
reject_cheque: '/acc/accounting/view/',
modify_cheque: '/acc/accounting/view/',
modify_cheque_output: '/acc/accounting/view/',
pass_cheque: '/acc/accounting/view/',
transfer_cheque: '/acc/accounting/view/',
2025-03-21 14:20:43 +03:30
};
return routes[type] + code;
},
getTypeLabel(type) {
const labels = {
sell: this.$t('pages.person_card.sell_invoice'),
buy: this.$t('pages.person_card.buy_invoice'),
rfbuy: this.$t('pages.person_card.return_buy'),
rfsell: this.$t('pages.person_card.return_sell'),
person_send: this.$t('pages.person_card.payment'),
person_receive: this.$t('pages.person_card.receipt'),
cost: this.$t('pages.person_card.cost'),
income: this.$t('pages.person_card.income'),
sell_receive: this.$t('pages.person_card.sell_receive'),
buy_send: this.$t('pages.person_card.buy_send'),
2025-04-12 18:50:34 +03:30
reject_cheque: this.$t('pages.person_card.reject_cheque'),
modify_cheque: this.$t('pages.person_card.modify_cheque'),
pass_cheque: this.$t('pages.person_card.pass_cheque'),
modify_cheque_output: this.$t('pages.person_card.modify_cheque_output'),
transfer_cheque: this.$t('pages.person_card.transfer_cheque'),
2025-03-21 14:20:43 +03:30
};
return labels[type] || type;
},
},
2025-08-11 15:20:24 +03:30
computed: {
snackbar: {
get() {
return this.$store.state.snackbar;
},
set(value) {
this.$store.commit('setSnackbar', value);
}
}
}
2025-03-21 14:20:43 +03:30
};
</script>
<style scoped>
.custom-header {
background-color: #f5f5f5 !important;
font-weight: bold !important;
}
.v-data-table {
border-radius: 8px;
overflow: hidden;
}
.v-card {
transition: all 0.3s ease;
}
.v-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1) !important;
}
.v-autocomplete {
background-color: white;
border-radius: 8px;
}
.v-toolbar {
border-bottom: 1px solid rgba(0,0,0,0.1);
}
.v-list-item {
transition: background-color 0.2s ease;
}
.v-list-item:hover {
background-color: #f5f5f5;
}
2025-03-21 14:20:43 +03:30
</style>