bug fix in cost controller list

This commit is contained in:
Hesabix 2025-03-26 18:10:38 +00:00
parent dd101c8ee2
commit 65d45e73d1
9 changed files with 746 additions and 629 deletions

View file

@ -166,160 +166,186 @@ class CostController extends AbstractController
#[Route('/api/cost/list/search', name: 'app_cost_list_search', methods: ['POST'])] #[Route('/api/cost/list/search', name: 'app_cost_list_search', methods: ['POST'])]
public function searchCostList( public function searchCostList(
Request $request, Request $request,
Access $access, Access $access,
EntityManagerInterface $entityManager, EntityManagerInterface $entityManager,
Jdate $jdate Jdate $jdate
): JsonResponse { ): JsonResponse {
$acc = $access->hasRole('cost'); $acc = $access->hasRole('cost');
if (!$acc) { if (!$acc) {
throw $this->createAccessDeniedException(); throw $this->createAccessDeniedException();
} }
$params = json_decode($request->getContent(), true) ?? []; $params = json_decode($request->getContent(), true) ?? [];
// پارامترهای ورودی // پارامترهای ورودی
$filters = $params['filters'] ?? []; $filters = $params['filters'] ?? [];
$pagination = $params['pagination'] ?? ['page' => 1, 'limit' => 10]; $pagination = $params['pagination'] ?? ['page' => 1, 'limit' => 10];
$sort = $params['sort'] ?? ['sortBy' => 'id', 'sortDesc' => true]; $sort = $params['sort'] ?? ['sortBy' => 'id', 'sortDesc' => true];
$type = $params['type'] ?? 'cost'; $type = $params['type'] ?? 'cost';
// تنظیم پارامترهای صفحه‌بندی // تنظیم پارامترهای صفحه‌بندی
$page = max(1, $pagination['page'] ?? 1); $page = max(1, $pagination['page'] ?? 1);
$limit = max(1, min(100, $pagination['limit'] ?? 10)); $limit = max(1, min(100, $pagination['limit'] ?? 10));
// ساخت کوئری پایه // ساخت کوئری پایه
$queryBuilder = $entityManager->createQueryBuilder() $queryBuilder = $entityManager->createQueryBuilder()
->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount') ->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount')
->addSelect('u.fullName as submitter') ->addSelect('u.fullName as submitter')
->from('App\Entity\HesabdariDoc', 'd') ->from('App\Entity\HesabdariDoc', 'd')
->leftJoin('d.submitter', 'u') ->leftJoin('d.submitter', 'u')
->where('d.bid = :bid') ->where('d.bid = :bid')
->andWhere('d.year = :year') ->andWhere('d.year = :year')
->andWhere('d.type = :type') ->andWhere('d.type = :type')
->andWhere('d.money = :money') ->andWhere('d.money = :money')
->setParameter('bid', $acc['bid']) ->setParameter('bid', $acc['bid'])
->setParameter('year', $acc['year']) ->setParameter('year', $acc['year'])
->setParameter('type', $type) ->setParameter('type', $type)
->setParameter('money', $acc['money']); ->setParameter('money', $acc['money']);
// اعمال فیلترها // اعمال فیلترها
if (!empty($filters)) { if (!empty($filters)) {
if (isset($filters['search'])) { // جستجوی متنی
$queryBuilder->leftJoin('d.hesabdariRows', 'r') if (isset($filters['search'])) {
->leftJoin('r.person', 'p') $searchValue = is_array($filters['search']) ? $filters['search']['value'] : $filters['search'];
->leftJoin('r.ref', 't') $queryBuilder->leftJoin('d.hesabdariRows', 'r')
->andWhere( ->leftJoin('r.person', 'p')
$queryBuilder->expr()->orX( ->leftJoin('r.ref', 't')
'd.code LIKE :search', ->andWhere(
'd.des LIKE :search', $queryBuilder->expr()->orX(
'd.date LIKE :search', 'd.code LIKE :search',
'd.amount LIKE :search', 'd.des LIKE :search',
'p.nikename LIKE :search', 'd.date LIKE :search',
't.name LIKE :search' 'd.amount LIKE :search',
) 'p.nikename LIKE :search',
) 't.name LIKE :search'
->setParameter('search', "%{$filters['search']}%"); )
} )
->setParameter('search', "%{$searchValue}%");
if (isset($filters['dateFrom'])) { }
$queryBuilder->andWhere('d.date >= :dateFrom')
->setParameter('dateFrom', $filters['dateFrom']); // فیلتر زمانی
} if (isset($filters['timeFilter'])) {
$today = $jdate->jdate('Y/m/d', time());
if (isset($filters['dateTo'])) { switch ($filters['timeFilter']) {
$queryBuilder->andWhere('d.date <= :dateTo') case 'today':
->setParameter('dateTo', $filters['dateTo']); $queryBuilder->andWhere('d.date = :today')
} ->setParameter('today', $today);
break;
if (isset($filters['amount'])) { case 'week':
$queryBuilder->andWhere('d.amount = :amount') $weekStart = $jdate->jdate('Y/m/d', strtotime('-6 days'));
->setParameter('amount', $filters['amount']); $queryBuilder->andWhere('d.date BETWEEN :weekStart AND :today')
} ->setParameter('weekStart', $weekStart)
} ->setParameter('today', $today);
break;
// اعمال مرتب‌سازی case 'month':
$sortField = $sort['sortBy'] ?? 'id'; $monthStart = $jdate->jdate('Y/m/01', time());
$sortDirection = ($sort['sortDesc'] ?? true) ? 'DESC' : 'ASC'; $queryBuilder->andWhere('d.date BETWEEN :monthStart AND :today')
$queryBuilder->orderBy("d.$sortField", $sortDirection); ->setParameter('monthStart', $monthStart)
->setParameter('today', $today);
// محاسبه تعداد کل نتایج break;
$totalItemsQuery = clone $queryBuilder; case 'custom':
$totalItems = $totalItemsQuery->select('COUNT(DISTINCT d.id)') if (isset($filters['dateFrom']) && isset($filters['dateTo'])) {
->getQuery() $queryBuilder->andWhere('d.date BETWEEN :dateFrom AND :dateTo')
->getSingleScalarResult(); ->setParameter('dateFrom', $filters['dateFrom'])
->setParameter('dateTo', $filters['dateTo']);
// اعمال صفحه‌بندی }
$queryBuilder->setFirstResult(($page - 1) * $limit) break;
->setMaxResults($limit); case 'all':
default:
$docs = $queryBuilder->getQuery()->getArrayResult(); // بدون فیلتر زمانی اضافه
break;
$dataTemp = []; }
foreach ($docs as $doc) { }
$item = [
'id' => $doc['id'], if (isset($filters['amount'])) {
'dateSubmit' => $doc['dateSubmit'], $queryBuilder->andWhere('d.amount = :amount')
'date' => $doc['date'], ->setParameter('amount', $filters['amount']);
'type' => $doc['type'], }
'code' => $doc['code'], }
'des' => $doc['des'],
'amount' => $doc['amount'], // اعمال مرتب‌سازی
'submitter' => $doc['submitter'] $sortField = is_array($sort['sortBy']) ? ($sort['sortBy']['key'] ?? 'id') : ($sort['sortBy'] ?? 'id');
]; $sortDirection = ($sort['sortDesc'] ?? true) ? 'DESC' : 'ASC';
$queryBuilder->orderBy("d.$sortField", $sortDirection);
// دریافت اطلاعات مرکز هزینه و مبلغ
$costDetails = $entityManager->createQueryBuilder() // محاسبه تعداد کل نتایج
->select('t.name as center_name, r.bd as amount') $totalItemsQuery = clone $queryBuilder;
->from('App\Entity\HesabdariRow', 'r') $totalItems = $totalItemsQuery->select('COUNT(DISTINCT d.id)')
->join('r.ref', 't') ->getQuery()
->where('r.doc = :docId') ->getSingleScalarResult();
->andWhere('r.bd != 0')
->setParameter('docId', $doc['id']) // اعمال صفحه‌بندی
->getQuery() $queryBuilder->setFirstResult(($page - 1) * $limit)
->getResult(); ->setMaxResults($limit);
$item['costCenters'] = array_map(function($detail) { $docs = $queryBuilder->getQuery()->getArrayResult();
return [
'name' => $detail['center_name'], $dataTemp = [];
'amount' => (int) $detail['amount'] foreach ($docs as $doc) {
]; $item = [
}, $costDetails); 'id' => $doc['id'],
'dateSubmit' => $doc['dateSubmit'],
// دریافت اطلاعات شخص مرتبط 'date' => $doc['date'],
$personInfo = $entityManager->createQueryBuilder() 'type' => $doc['type'],
->select('p.id, p.nikename, p.code') 'code' => $doc['code'],
->from('App\Entity\HesabdariRow', 'r') 'des' => $doc['des'],
->join('r.person', 'p') 'amount' => $doc['amount'],
->where('r.doc = :docId') 'submitter' => $doc['submitter'],
->andWhere('r.person IS NOT NULL') ];
->setParameter('docId', $doc['id'])
->setMaxResults(1) // دریافت اطلاعات مرکز هزینه و مبلغ
->getQuery() $costDetails = $entityManager->createQueryBuilder()
->getOneOrNullResult(); ->select('t.name as center_name, r.bd as amount')
->from('App\Entity\HesabdariRow', 'r')
$item['person'] = $personInfo ? [ ->join('r.ref', 't')
'id' => $personInfo['id'], ->where('r.doc = :docId')
'nikename' => $personInfo['nikename'], ->andWhere('r.bd != 0')
'code' => $personInfo['code'] ->setParameter('docId', $doc['id'])
] : null; ->getQuery()
->getResult();
$dataTemp[] = $item;
} $item['costCenters'] = array_map(function ($detail) {
return [
return $this->json([ 'name' => $detail['center_name'],
'items' => $dataTemp, 'amount' => (int) $detail['amount'],
'total' => (int) $totalItems, ];
'page' => $page, }, $costDetails);
'limit' => $limit
]); // دریافت اطلاعات شخص مرتبط
$personInfo = $entityManager->createQueryBuilder()
->select('p.id, p.nikename, p.code')
->from('App\Entity\HesabdariRow', 'r')
->join('r.person', 'p')
->where('r.doc = :docId')
->andWhere('r.person IS NOT NULL')
->setParameter('docId', $doc['id'])
->setMaxResults(1 )
->getQuery()
->getOneOrNullResult();
$item['person'] = $personInfo ? [
'id' => $personInfo['id'],
'nikename' => $personInfo['nikename'],
'code' => $personInfo['code'],
] : null;
$dataTemp[] = $item;
}
return $this->json([
'items' => $dataTemp,
'total' => (int) $totalItems,
'page' => $page,
'limit' => $limit,
]);
} }
#[Route('/api/costs/list/print', name: 'app_costs_list_print')] #[Route('/api/costs/list/print', name: 'app_costs_list_print')]
public function app_costs_list_print( public function app_costs_list_print(
Provider $provider, Provider $provider,
Request $request, Request $request,
Access $access, Access $access,
EntityManagerInterface $entityManager EntityManagerInterface $entityManager
): JsonResponse { ): JsonResponse {
$acc = $access->hasRole('cost'); $acc = $access->hasRole('cost');
@ -328,7 +354,7 @@ class CostController extends AbstractController
} }
$params = json_decode($request->getContent(), true) ?? []; $params = json_decode($request->getContent(), true) ?? [];
// دریافت آیتم‌های انتخاب شده یا همه آیتم‌ها // دریافت آیتم‌های انتخاب شده یا همه آیتم‌ها
if (!isset($params['items'])) { if (!isset($params['items'])) {
$items = $entityManager->getRepository(HesabdariDoc::class)->findBy([ $items = $entityManager->getRepository(HesabdariDoc::class)->findBy([
@ -411,12 +437,12 @@ class CostController extends AbstractController
// تنظیم هدرها // تنظیم هدرها
$sheet->setCellValue('A1', 'ردیف') $sheet->setCellValue('A1', 'ردیف')
->setCellValue('B1', 'شماره سند') ->setCellValue('B1', 'شماره سند')
->setCellValue('C1', 'تاریخ') ->setCellValue('C1', 'تاریخ')
->setCellValue('D1', 'شرح') ->setCellValue('D1', 'شرح')
->setCellValue('E1', 'مرکز هزینه') ->setCellValue('E1', 'مرکز هزینه')
->setCellValue('F1', 'مرکز پرداخت') ->setCellValue('F1', 'مرکز پرداخت')
->setCellValue('G1', 'مبلغ (ریال)'); ->setCellValue('G1', 'مبلغ (ریال)');
// پر کردن داده‌ها // پر کردن داده‌ها
$rowNumber = 2; $rowNumber = 2;
@ -449,12 +475,12 @@ class CostController extends AbstractController
} }
$sheet->setCellValue('A' . $rowNumber, $index + 1) $sheet->setCellValue('A' . $rowNumber, $index + 1)
->setCellValue('B' . $rowNumber, $item->getCode()) ->setCellValue('B' . $rowNumber, $item->getCode())
->setCellValue('C' . $rowNumber, $item->getDate()) ->setCellValue('C' . $rowNumber, $item->getDate())
->setCellValue('D' . $rowNumber, $item->getDes()) ->setCellValue('D' . $rowNumber, $item->getDes())
->setCellValue('E' . $rowNumber, $costCenterNames) ->setCellValue('E' . $rowNumber, $costCenterNames)
->setCellValue('F' . $rowNumber, $paymentCenter) ->setCellValue('F' . $rowNumber, $paymentCenter)
->setCellValue('G' . $rowNumber, number_format($item->getAmount())); ->setCellValue('G' . $rowNumber, number_format($item->getAmount()));
$rowNumber++; $rowNumber++;
} }

View file

@ -15,6 +15,7 @@
"@ckeditor/ckeditor5-image": "^36.0.1", "@ckeditor/ckeditor5-image": "^36.0.1",
"@ckeditor/ckeditor5-vue": "^4.0.1", "@ckeditor/ckeditor5-vue": "^4.0.1",
"@ckeditor/vite-plugin-ckeditor5": "^0.1.1", "@ckeditor/vite-plugin-ckeditor5": "^0.1.1",
"@date-io/date-fns-jalali": "^3.2.0",
"@mdi/font": "^7.4.47", "@mdi/font": "^7.4.47",
"@syncfusion/ej2-vue-dropdowns": "^21.2.5", "@syncfusion/ej2-vue-dropdowns": "^21.2.5",
"@vuelidate/core": "^2.0.0", "@vuelidate/core": "^2.0.0",
@ -23,8 +24,11 @@
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"apexcharts": "^4.4.0", "apexcharts": "^4.4.0",
"axios": "^1.2.3", "axios": "^1.2.3",
"date-fns": "^4.1.0",
"date-fns-jalali": "^3.2.0-0",
"downloadjs": "^1.4.7", "downloadjs": "^1.4.7",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jalali-moment": "^3.3.11",
"libphonenumber-js": "^1.10.44", "libphonenumber-js": "^1.10.44",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"maska": "^3.0.4", "maska": "^3.0.4",
@ -66,4 +70,4 @@
"vue-tsc": "^1.0.12" "vue-tsc": "^1.0.12"
}, },
"build:pwa": "vue-cli-service build && workbox generateSW workbox-config.js" "build:pwa": "vue-cli-service build && workbox generateSW workbox-config.js"
} }

View file

@ -1,53 +1,95 @@
<script lang="ts">
import {ref, watch, computed, defineComponent} from 'vue'
export default defineComponent({
name:'Hdatepicker',
inheritAttrs: false,
data:()=>{
const self = this;
return{
isMenuOpen: ref(false),
selectedDate: ref(),
output:''
}
},
watch:{
selectedDate(){
this.isMenuOpen = false
this.output = this.selectedDate.toLocaleString(
localStorage.getItem('UI_LANG'),
{year: 'numeric', month: 'numeric', day: 'numeric'}
)
}
},
setup(props, ctx) {
},
})
</script>
<template> <template>
<v-menu v-model="isMenuOpen" :close-on-content-click="false"> <div>
<template v-slot:activator="{ props }"> <v-text-field v-model="displayDate" :label="label" prepend-inner-icon="mdi-calendar" persistent-placeholder
<v-text-field class="v-date-input" :rules="rules" @input="updateDateFromInput" @click:prepend="togglePicker"></v-text-field>
v-model="output" <date-picker v-model="displayDate" type="date" format="jYYYY/jMM/jDD" display-format="jYYYY/jMM/jDD"
readonly :min="minDatePersian" :max="maxDatePersian" custom-input=".v-date-input" :input-mode="false"
v-bind="props" :editable="pickerActive" @close="pickerActive = false"></date-picker>
:value="output" </div>
outlined
color="primary"
@input="(v: any) => $emit('input', v)"
></v-text-field>
</template>
<!-- !!! hide-actions prop too !!! -->
<v-date-picker color="indigo" v-model="selectedDate">
<template v-slot:header></template>
</v-date-picker>
</v-menu>
</template> </template>
<style scoped> <script>
import axios from 'axios';
import moment from 'jalali-moment';
export default {
props: {
value: {
type: String,
default: '',
},
label: {
type: String,
default: 'تاریخ',
},
rules: {
type: Array,
default: () => [],
},
},
data() {
return {
displayDate: '', // تاریخ به فرمت شمسی
pickerActive: false, // کنترل باز شدن تقویم
minDatePersian: '', // تاریخ شروع سال مالی (شمسی برای پکیج)
maxDatePersian: '', // تاریخ پایان سال مالی (شمسی برای پکیج)
};
},
watch: {
displayDate(newVal) {
if (newVal) {
this.$emit('input', newVal); // ارسال تاریخ شمسی به والد
} else {
this.$emit('input', '');
}
},
value(newVal) {
if (newVal) {
this.displayDate = newVal;
} else {
this.displayDate = '';
}
},
},
async mounted() {
await this.fetchYearData();
if (!this.value && this.displayDate) {
this.$emit('input', this.displayDate);
}
},
methods: {
async fetchYearData() {
axios.get('/api/year/get').then((response) => {
this.minDatePersian = response.data.start; // فرمت YYYY/MM/DD شمسی
this.maxDatePersian = response.data.end; // فرمت YYYY/MM/DD شمسی
this.displayDate = response.data.now; // تاریخ جاری شمسی
});
},
updateDateFromInput(value) {
// بررسی و اعتبارسنجی تاریخ وارد شده توسط کاربر
if (value && moment(value, 'YYYY/MM/DD', 'fa', true).isValid()) {
const parsedDate = moment(value, 'YYYY/MM/DD').locale('fa');
if (
parsedDate.isSameOrAfter(moment(this.minDatePersian, 'YYYY/MM/DD')) &&
parsedDate.isSameOrBefore(moment(this.maxDatePersian, 'YYYY/MM/DD'))
) {
this.displayDate = value;
} else {
this.displayDate = ''; // یا خطا نمایش بدید
}
}
},
togglePicker() {
this.pickerActive = !this.pickerActive; // تغییر وضعیت تقویم
},
},
};
</script>
<style scoped>
/* مطمئن شدن که تقویم فقط با آیکون فعال بشه */
.v-date-input {
position: relative;
}
</style> </style>

View file

@ -1,4 +1,3 @@
import moment from 'moment'
export default { export default {
data() { data() {
return { return {

View file

@ -13,7 +13,7 @@ if(activeLanguageCode == null || activeLanguageCode == undefined){
activeLanguageCode='fa'; activeLanguageCode='fa';
} }
const i18n = createI18n({ const i18n = createI18n({
legacy: false, // Vuetify does not support the legacy mode of vue-i18n legacy: false,
locale: activeLanguageCode, locale: activeLanguageCode,
fallbackLocale: activeLanguageCode, fallbackLocale: activeLanguageCode,
messages, messages,

View file

@ -6,13 +6,13 @@ import "./registerServiceWorker";
import { vMaska } from "maska/vue" import { vMaska } from "maska/vue"
import VueApexCharts from "vue3-apexcharts"; import VueApexCharts from "vue3-apexcharts";
import Uploader from 'vue-media-upload'; import Uploader from 'vue-media-upload';
import DateFnsJalaliAdapter from '@date-io/date-fns-jalali';
import faIR from 'date-fns-jalali/locale/fa-IR';
//pinia //pinia
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
const pinia = createPinia(); const pinia = createPinia();
import { VDateInput } from 'vuetify/labs/VDateInput'
import CKEditor from '@ckeditor/ckeditor5-vue'; import CKEditor from '@ckeditor/ckeditor5-vue';
// Import translations for the Persian language. // Import translations for the Persian language.
import '@ckeditor/ckeditor5-build-classic/build/translations/fa'; import '@ckeditor/ckeditor5-build-classic/build/translations/fa';
@ -124,6 +124,12 @@ const vuetify = createVuetify({
}, },
}, },
}, },
date: {
adapter: DateFnsJalaliAdapter,
locale: {
fa:faIR // تنظیم زبان فارسی
},
},
}); });
// @ts-ignore // @ts-ignore
@ -146,7 +152,6 @@ import 'vue-select/dist/vue-select.css';
app.component('v-cob', vSelect) app.component('v-cob', vSelect)
import Hdatepicker from "@/components/forms/Hdatepicker.vue"; import Hdatepicker from "@/components/forms/Hdatepicker.vue";
import calendarLocalConfig from "@/i18n/calendarLocalConfig"; import calendarLocalConfig from "@/i18n/calendarLocalConfig";
app.component('h-date-picker', Hdatepicker); app.component('h-date-picker', Hdatepicker);
app.use(CKEditor) app.use(CKEditor)
app.use(Vue3PersianDatetimePicker, { app.use(Vue3PersianDatetimePicker, {

View file

@ -333,7 +333,7 @@ export default {
<v-list-item-title> <v-list-item-title>
{{ $t('drawer.gets') }} {{ $t('drawer.gets') }}
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/persons/receive/list') <span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/persons/receive/list')
}}</span> }}</span>
</v-list-item-title> </v-list-item-title>
<template v-slot:append> <template v-slot:append>
<v-tooltip :text="$t('dialog.add_new')" location="end"> <v-tooltip :text="$t('dialog.add_new')" location="end">
@ -385,7 +385,7 @@ export default {
<v-list-item-title> <v-list-item-title>
{{ $t('drawer.price_lists') }} {{ $t('drawer.price_lists') }}
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/commodity/pricelist/list') <span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/commodity/pricelist/list')
}}</span> }}</span>
</v-list-item-title> </v-list-item-title>
<template v-slot:append> <template v-slot:append>
<v-tooltip :text="$t('dialog.add_new')" location="end"> <v-tooltip :text="$t('dialog.add_new')" location="end">
@ -509,7 +509,7 @@ export default {
<v-list-item-title> <v-list-item-title>
{{ $t('drawer.storeroom_ticket') }} {{ $t('drawer.storeroom_ticket') }}
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/storeroom/tickets/list') <span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/storeroom/tickets/list')
}}</span> }}</span>
</v-list-item-title> </v-list-item-title>
<template v-slot:append> <template v-slot:append>
<v-tooltip :text="$t('dialog.add_new')" location="end"> <v-tooltip :text="$t('dialog.add_new')" location="end">
@ -524,7 +524,7 @@ export default {
{{ $t('drawer.commodity_exist_count') }} {{ $t('drawer.commodity_exist_count') }}
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ <span v-if="isCtrlShiftPressed" class="shortcut-key">{{
getShortcutKey('/acc/storeroom/commodity/check/exist') getShortcutKey('/acc/storeroom/commodity/check/exist')
}}</span> }}</span>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
</v-list-group> </v-list-group>
@ -647,21 +647,19 @@ export default {
{{ $t('drawer.accounting_docs') }} {{ $t('drawer.accounting_docs') }}
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/accounting/list') }}</span> <span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/accounting/list') }}</span>
</v-list-item-title> </v-list-item-title>
<template v-slot:append> <template v-slot:append v-if="isPluginActive('accpro') && 1==2">
<!--
<v-tooltip :text="$t('dialog.add_new')" location="end"> <v-tooltip :text="$t('dialog.add_new')" location="end">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-plus-box" variant="plain" to="/acc/accounting/mod/" /> <v-btn v-bind="props" icon="mdi-plus-box" variant="plain" to="/acc/accounting/mod/" />
</template> </template>
</v-tooltip> </v-tooltip>
-->
</template> </template>
</v-list-item> </v-list-item>
<v-list-item v-if="permissions.accounting" to="/acc/accounting/open_balance"> <v-list-item v-if="permissions.accounting" to="/acc/accounting/open_balance">
<v-list-item-title> <v-list-item-title>
{{ $t('drawer.open_balance') }} {{ $t('drawer.open_balance') }}
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/accounting/open_balance') <span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/accounting/open_balance')
}}</span> }}</span>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item v-if="permissions.accounting" to="/acc/accounting/table"> <v-list-item v-if="permissions.accounting" to="/acc/accounting/table">
@ -675,7 +673,7 @@ export default {
<v-list-item-title> <v-list-item-title>
{{ $t('drawer.close_year') }} {{ $t('drawer.close_year') }}
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/accounting/close_year') <span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/accounting/close_year')
}}</span> }}</span>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
</v-list-group> </v-list-group>
@ -702,7 +700,7 @@ export default {
<v-list-item-title> <v-list-item-title>
{{ $t('drawer.print_settings') }} {{ $t('drawer.print_settings') }}
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/business/printoptions') <span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/business/printoptions')
}}</span> }}</span>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item v-if="permissions.settings && this.isPluginActive('accpro')" to="/acc/business/avatar"> <v-list-item v-if="permissions.settings && this.isPluginActive('accpro')" to="/acc/business/avatar">
@ -727,7 +725,7 @@ export default {
<v-list-item-title> <v-list-item-title>
{{ $t('drawer.extra_moneys') }} {{ $t('drawer.extra_moneys') }}
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/business/extramoneys') <span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/business/extramoneys')
}}</span> }}</span>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item v-if="permissions.log" to="/acc/business/logs"> <v-list-item v-if="permissions.log" to="/acc/business/logs">
@ -751,7 +749,7 @@ export default {
<v-list-item-title> <v-list-item-title>
{{ $t('drawer.repservice_reqs') }} {{ $t('drawer.repservice_reqs') }}
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/plugin/repservice/order/list') <span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/plugin/repservice/order/list')
}}</span> }}</span>
</v-list-item-title> </v-list-item-title>
<template v-slot:append> <template v-slot:append>
<v-tooltip :text="$t('dialog.add_new')" location="end"> <v-tooltip :text="$t('dialog.add_new')" location="end">
@ -842,7 +840,7 @@ export default {
<v-list-item-title> <v-list-item-title>
{{ $t('drawer.plugins_invoices') }} {{ $t('drawer.plugins_invoices') }}
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/plugin-center/invoice') <span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/plugin-center/invoice')
}}</span> }}</span>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
</v-list-group> </v-list-group>

View file

@ -1,238 +1,251 @@
<template> <template>
<v-container> <v-container>
<v-form @submit.prevent="submitForm"> <v-form @submit.prevent="submitForm">
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <Hdatepicker
v-model="form.date" v-model="form.date"
label="تاریخ (شمسی)" :rules="[v => !!v || 'تاریخ الزامی است']"
placeholder="1403/02/28" />
:rules="[v => !!v || 'تاریخ الزامی است']" </v-col>
></v-text-field> <v-col cols="12" md="6">
</v-col> <v-text-field
</v-row> v-model="form.des"
label="توضیحات سند"
<v-data-table placeholder="توضیحات مربوط به سند"
:headers="headers" ></v-text-field>
:items="form.rows" </v-col>
class="elevation-1" </v-row>
hide-default-footer
> <v-data-table
<template v-slot:top> :headers="headers"
<v-toolbar flat> :items="form.rows"
<v-toolbar-title>ردیفهای سند</v-toolbar-title> class="elevation-1"
<v-spacer></v-spacer> hide-default-footer
<v-btn color="primary" @click="addRow">افزودن ردیف</v-btn> >
</v-toolbar> <template v-slot:top>
</template> <v-toolbar flat>
<v-toolbar-title>ردیفهای سند</v-toolbar-title>
<template v-slot:item.ref="{ item }"> <v-spacer></v-spacer>
<v-menu offset-y> <v-btn color="primary" @click="addRow">افزودن ردیف</v-btn>
<template v-slot:activator="{ props }"> </v-toolbar>
<v-text-field </template>
v-model="item.refName"
label="حساب" <template v-slot:item.ref="{ item }">
dense <v-menu offset-y>
readonly <template v-slot:activator="{ props }">
v-bind="props" <v-text-field
:rules="[v => !!item.ref || 'حساب الزامی است']" v-model="item.refName"
></v-text-field> label="حساب"
dense
readonly
v-bind="props"
:rules="[v => !!item.ref || 'حساب الزامی است']"
></v-text-field>
</template>
<v-treeview
:items="hesabdariTables"
item-key="id"
item-text="name"
item-children="children"
selectable
return-object
v-model="item.selectedAccounts"
@update:active="selectAccount(item, $event)"
>
<template v-slot:label="{ item: treeItem }">
{{ treeItem.name }}
</template> </template>
<v-treeview </v-treeview>
:items="hesabdariTables" </v-menu>
item-key="id" </template>
item-text="name"
item-children="children" <template v-slot:item.bd="{ item }">
selectable <v-text-field
return-object v-model="item.bd"
v-model="item.selectedAccounts" label="بدهکار"
@update:active="selectAccount(item, $event)" type="number"
> dense
<template v-slot:label="{ item: treeItem }"> @input="calculateTotals"
{{ treeItem.name }} ></v-text-field>
</template> </template>
</v-treeview>
</v-menu> <template v-slot:item.bs="{ item }">
</template> <v-text-field
v-model="item.bs"
<template v-slot:item.bd="{ item }"> label="بستانکار"
<v-text-field type="number"
v-model="item.bd" dense
label="بدهکار" @input="calculateTotals"
type="number" ></v-text-field>
dense </template>
@input="calculateTotals"
></v-text-field> <template v-slot:item.des="{ item }">
</template> <v-text-field v-model="item.des" label="توضیحات" dense></v-text-field>
</template>
<template v-slot:item.bs="{ item }">
<v-text-field <template v-slot:item.actions="{ item }">
v-model="item.bs" <v-btn color="error" small @click="removeRow(item)">حذف</v-btn>
label="بستانکار" </template>
type="number" </v-data-table>
dense
@input="calculateTotals" <v-row class="mt-4">
></v-text-field> <v-col cols="6">
</template> <v-text-field
:value="totalBd"
<template v-slot:item.des="{ item }"> label="جمع بدهکار"
<v-text-field v-model="item.des" label="توضیحات" dense></v-text-field> readonly
</template> dense
></v-text-field>
<template v-slot:item.actions="{ item }"> </v-col>
<v-btn color="error" small @click="removeRow(item)">حذف</v-btn> <v-col cols="6">
</template> <v-text-field
</v-data-table> :value="totalBs"
label="جمع بستانکار"
<v-row class="mt-4"> readonly
<v-col cols="6"> dense
<v-text-field ></v-text-field>
:value="totalBd" </v-col>
label="جمع بدهکار" </v-row>
readonly
dense <v-alert v-if="error" type="error" class="mt-4">{{ error }}</v-alert>
></v-text-field>
</v-col> <v-btn type="submit" color="success" class="mt-4" :disabled="totalBd !== totalBs || !form.date">
<v-col cols="6"> ثبت سند
<v-text-field </v-btn>
:value="totalBs" </v-form>
label="جمع بستانکار" </v-container>
readonly </template>
dense
></v-text-field> <script>
</v-col> import axios from 'axios';
</v-row> import moment from 'jalali-moment';
import Hdatepicker from '@/components/forms/Hdatepicker.vue';
<v-alert v-if="error" type="error" class="mt-4">{{ error }}</v-alert> export default {
components: {
<v-btn type="submit" color="success" class="mt-4" :disabled="totalBd !== totalBs || !form.date"> Hdatepicker,
ثبت سند },
</v-btn> props: {
</v-form> docId: {
</v-container> type: Number,
</template> default: null,
<script>
import axios from 'axios';
export default {
props: {
docId: {
type: Number,
default: null,
},
}, },
data() { },
return { data() {
form: { return {
date: '', form: {
rows: [ date: '', // تاریخ به فرمت ISO (مثلاً 2025-03-24)
{ ref: null, refName: '', bd: '0', bs: '0', des: '', selectedAccounts: [] }, des: '',
], rows: [
}, { ref: null, refName: '', bd: '0', bs: '0', des: '', selectedAccounts: [] },
hesabdariTables: [],
totalBd: 0,
totalBs: 0,
error: null,
headers: [
{ text: 'حساب', value: 'ref' },
{ text: 'بدهکار', value: 'bd' },
{ text: 'بستانکار', value: 'bs' },
{ text: 'توضیحات', value: 'des' },
{ text: 'عملیات', value: 'actions', sortable: false },
], ],
}; },
}, hesabdariTables: [],
mounted() { totalBd: 0,
this.fetchHesabdariTables(); totalBs: 0,
if (this.docId) { error: null,
this.fetchDoc(); headers: [
{ text: 'حساب', value: 'ref' },
{ text: 'بدهکار', value: 'bd' },
{ text: 'بستانکار', value: 'bs' },
{ text: 'توضیحات', value: 'des' },
{ text: 'عملیات', value: 'actions', sortable: false },
],
};
},
mounted() {
this.fetchHesabdariTables();
if (this.docId) {
this.fetchDoc();
}
},
methods: {
async fetchHesabdariTables() {
try {
const response = await axios.get('/api/hesabdari/tables');
this.hesabdariTables = response.data.data;
} catch (error) {
console.error('خطا در دریافت حساب‌ها:', error.response?.data || error.message);
this.error = 'خطا در بارگذاری حساب‌ها: ' + (error.response?.data?.message || 'مشکل ناشناخته');
} }
}, },
methods: { async fetchDoc() {
async fetchHesabdariTables() { try {
try { const response = await axios.get(`/api/hesabdari/doc/${this.docId}`);
const response = await axios.get('/api/hesabdari/tables'); const serverDate = response.data.data.date; // فرض: تاریخ شمسی از سرور
this.hesabdariTables = response.data.data; this.form.date = moment(serverDate, 'YYYY/MM/DD').format('YYYY-MM-DD');
} catch (error) { this.form.des = response.data.data.des || '';
console.error('خطا در دریافت حساب‌ها:', error.response?.data || error.message); this.form.rows = response.data.data.rows.map(row => ({
this.error = 'خطا در بارگذاری حساب‌ها: ' + (error.response?.data?.message || 'مشکل ناشناخته'); ref: row.ref.id,
} refName: row.ref.name,
}, bd: row.bd,
async fetchDoc() { bs: row.bs,
try { des: row.des,
const response = await axios.get(`/api/hesabdari/doc/${this.docId}`); selectedAccounts: [{ id: row.ref.id, name: row.ref.name }],
this.form.date = response.data.data.date; }));
this.form.rows = response.data.data.rows.map(row => ({ this.calculateTotals();
ref: row.ref.id, } catch (error) {
refName: row.ref.name, this.error = 'خطا در بارگذاری سند: ' + (error.response?.data?.message || 'مشکل ناشناخته');
bd: row.bd, }
bs: row.bs,
des: row.des,
selectedAccounts: [{ id: row.ref.id, name: row.ref.name }],
}));
this.calculateTotals();
} catch (error) {
this.error = 'خطا در بارگذاری سند: ' + (error.response?.data?.message || 'مشکل ناشناخته');
}
},
addRow() {
this.form.rows.push({ ref: null, refName: '', bd: '0', bs: '0', des: '', selectedAccounts: [] });
},
removeRow(item) {
const index = this.form.rows.indexOf(item);
if (index >= 0) {
this.form.rows.splice(index, 1);
this.calculateTotals();
}
},
calculateTotals() {
this.totalBd = this.form.rows.reduce((sum, row) => sum + parseInt(row.bd || 0), 0);
this.totalBs = this.form.rows.reduce((sum, row) => sum + parseInt(row.bs || 0), 0);
},
selectAccount(row, selected) {
if (selected.length > 0) {
const account = selected[0];
row.ref = account.id;
row.refName = account.name;
row.selectedAccounts = [account];
}
},
async submitForm() {
this.error = null;
if (this.totalBd !== this.totalBs) {
this.error = 'جمع بدهکار و بستانکار باید برابر باشد';
return;
}
const payload = {
date: this.form.date,
rows: this.form.rows.map(row => ({
ref: row.ref,
bd: row.bd,
bs: row.bs,
des: row.des,
})),
};
try {
if (this.docId) {
await axios.put(`/api/hesabdari/doc/${this.docId}`, payload);
this.$emit('saved', 'سند با موفقیت ویرایش شد');
} else {
const response = await axios.post('/api/hesabdari/doc', payload);
this.$emit('saved', 'سند با موفقیت ثبت شد', response.data.data.id);
}
} catch (error) {
this.error = error.response?.data?.message || 'خطا در ثبت سند';
}
},
}, },
}; addRow() {
</script> this.form.rows.push({ ref: null, refName: '', bd: '0', bs: '0', des: '', selectedAccounts: [] });
},
<style scoped> removeRow(item) {
.v-data-table { const index = this.form.rows.indexOf(item);
margin-top: 20px; if (index >= 0) {
} this.form.rows.splice(index, 1);
</style> this.calculateTotals();
}
},
calculateTotals() {
this.totalBd = this.form.rows.reduce((sum, row) => sum + parseInt(row.bd || 0), 0);
this.totalBs = this.form.rows.reduce((sum, row) => sum + parseInt(row.bs || 0), 0);
},
selectAccount(row, selected) {
if (selected.length > 0) {
const account = selected[0];
row.ref = account.id;
row.refName = account.name;
row.selectedAccounts = [account];
}
},
async submitForm() {
this.error = null;
if (this.totalBd !== this.totalBs) {
this.error = 'جمع بدهکار و بستانکار باید برابر باشد';
return;
}
const payload = {
date: moment(this.form.date, 'YYYY-MM-DD').locale('fa').format('YYYY/MM/DD'), // ارسال به فرمت شمسی
des: this.form.des,
rows: this.form.rows.map(row => ({
ref: row.ref,
bd: row.bd,
bs: row.bs,
des: row.des,
})),
};
try {
if (this.docId) {
await axios.put(`/api/hesabdari/doc/${this.docId}`, payload);
this.$emit('saved', 'سند با موفقیت ویرایش شد');
} else {
const response = await axios.post('/api/hesabdari/doc', payload);
this.$emit('saved', 'سند با موفقیت ثبت شد', response.data.data.id);
}
} catch (error) {
this.error = error.response?.data?.message || 'خطا در ثبت سند';
}
},
},
};
</script>
<style scoped>
.v-data-table {
margin-top: 20px;
}
</style>

View file

@ -3,141 +3,116 @@
<template v-slot:prepend> <template v-slot:prepend>
<v-tooltip :text="$t('dialog.back')" location="bottom"> <v-tooltip :text="$t('dialog.back')" location="bottom">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text" icon="mdi-arrow-right" /> <v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text"
icon="mdi-arrow-right" />
</template> </template>
</v-tooltip> </v-tooltip>
</template> </template>
<v-spacer /> <v-spacer />
<v-slide-group show-arrows>
<v-slide-group-item>
<v-tooltip :text="$t('dialog.add_new')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-plus" color="primary" to="/acc/costs/mod/" />
</template>
</v-tooltip>
</v-slide-group-item>
<v-slide-group-item> <v-tooltip :text="$t('dialog.add_new')" location="bottom">
<v-menu> <template v-slot:activator="{ props }">
<template v-slot:activator="{ props }"> <v-btn v-bind="props" icon="mdi-plus" color="primary" to="/acc/costs/mod/" />
<v-btn </template>
v-bind="props" </v-tooltip>
icon=""
color="red"
>
<v-tooltip activator="parent" :text="$t('dialog.export_pdf')" location="bottom" />
<v-icon icon="mdi-file-pdf-box" />
</v-btn>
</template>
<v-list>
<v-list-subheader color="primary">{{ $t('dialog.export_pdf') }}</v-list-subheader>
<v-list-item :disabled="!hasSelected" class="text-dark" :title="$t('dialog.selected')" @click="exportPDF(false)">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-check" />
</template>
</v-list-item>
<v-list-item class="text-dark" :title="$t('dialog.all')" @click="exportPDF(true)">
<template v-slot:prepend>
<v-icon color="indigo-darken-4" icon="mdi-expand-all" />
</template>
</v-list-item>
</v-list>
</v-menu>
</v-slide-group-item>
<v-slide-group-item> <v-menu>
<v-menu> <template v-slot:activator="{ props }">
<template v-slot:activator="{ props }"> <v-btn v-bind="props" icon="" color="red">
<v-btn <v-tooltip activator="parent" :text="$t('dialog.export_pdf')" location="bottom" />
v-bind="props" <v-icon icon="mdi-file-pdf-box" />
icon="" </v-btn>
color="green" </template>
> <v-list>
<v-tooltip activator="parent" :text="$t('dialog.export_excel')" location="bottom" /> <v-list-subheader color="primary">{{ $t('dialog.export_pdf') }}</v-list-subheader>
<v-icon icon="mdi-file-excel-box" /> <v-list-item :disabled="!hasSelected" class="text-dark" :title="$t('dialog.selected')"
</v-btn> @click="exportPDF(false)">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-check" />
</template> </template>
<v-list> </v-list-item>
<v-list-subheader color="primary">{{ $t('dialog.export_excel') }}</v-list-subheader> <v-list-item class="text-dark" :title="$t('dialog.all')" @click="exportPDF(true)">
<v-list-item :disabled="!hasSelected" class="text-dark" :title="$t('dialog.selected')" @click="exportExcel(false)"> <template v-slot:prepend>
<template v-slot:prepend> <v-icon color="indigo-darken-4" icon="mdi-expand-all" />
<v-icon color="green-darken-4" icon="mdi-check" /> </template>
</template> </v-list-item>
</v-list-item> </v-list>
<v-list-item class="text-dark" :title="$t('dialog.all')" @click="exportExcel(true)"> </v-menu>
<template v-slot:prepend>
<v-icon color="indigo-darken-4" icon="mdi-expand-all" />
</template>
</v-list-item>
</v-list>
</v-menu>
</v-slide-group-item>
<v-slide-group-item> <v-menu>
<v-tooltip :text="$t('dialog.delete')" location="bottom"> <template v-slot:activator="{ props }">
<template v-slot:activator="{ props }"> <v-btn v-bind="props" icon="" color="green">
<v-btn <v-tooltip activator="parent" :text="$t('dialog.export_excel')" location="bottom" />
v-bind="props" <v-icon icon="mdi-file-excel-box" />
icon="mdi-trash-can" </v-btn>
color="danger" </template>
@click="deleteGroup" <v-list>
:disabled="!hasSelected" <v-list-subheader color="primary">{{ $t('dialog.export_excel') }}</v-list-subheader>
/> <v-list-item :disabled="!hasSelected" class="text-dark" :title="$t('dialog.selected')"
@click="exportExcel(false)">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-check" />
</template> </template>
</v-tooltip> </v-list-item>
</v-slide-group-item> <v-list-item class="text-dark" :title="$t('dialog.all')" @click="exportExcel(true)">
</v-slide-group> <template v-slot:prepend>
<v-icon color="indigo-darken-4" icon="mdi-expand-all" />
</template>
</v-list-item>
</v-list>
</v-menu>
<v-tooltip :text="$t('dialog.delete')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-trash-can" color="danger" @click="deleteGroup" :disabled="!hasSelected" />
</template>
</v-tooltip>
</v-toolbar> </v-toolbar>
<v-text-field <v-text-field :loading="loading" color="green" class="mb-0 pt-0 rounded-0" hide-details="auto" density="compact"
:loading="loading" :placeholder="$t('dialog.search_txt')" v-model="searchQuery" type="text" clearable>
color="green"
class="mb-0 pt-0 rounded-0"
hide-details="auto"
density="compact"
:placeholder="$t('dialog.search_txt')"
v-model="searchQuery"
type="text"
@input="debouncedSearch"
>
<template v-slot:prepend-inner> <template v-slot:prepend-inner>
<v-tooltip location="bottom" :text="$t('dialog.search')"> <v-tooltip location="bottom" :text="$t('dialog.search')">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-icon v-bind="props" color="danger" icon="mdi-magnify" /> <v-icon v-bind="props" color="danger" icon="mdi-magnify" />
</template> </template>
</v-tooltip> </v-tooltip>
</template>
<template v-slot:append-inner>
<v-menu :close-on-content-click="false">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" size="sm" color="primary">
<v-icon>mdi-filter</v-icon>
<v-tooltip activator="parent" :text="$t('dialog.filters')" location="bottom" />
</v-icon>
</template>
<v-list>
<v-list-subheader color="primary">
<v-icon>mdi-filter</v-icon>
{{ $t('dialog.filters') }}
</v-list-subheader>
<v-list-item v-for="(filter, index) in timeFilters" :key="index" class="text-dark">
<template v-slot:title>
<v-checkbox v-model="filter.checked" :label="filter.label" @change="applyTimeFilter(filter.value)"
hide-details />
</template> </template>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-text-field> </v-text-field>
<v-data-table-server <v-data-table-server :headers="headers" :items="items" :loading="loading" :items-length="totalItems"
:headers="headers" v-model:options="serverOptions" @update:options="fetchData" item-value="code" class="elevation-1 data-table-wrapper"
:items="items" :header-props="{ class: 'custom-header' }">
:loading="loading"
:items-length="totalItems"
v-model:options="serverOptions"
@update:options="fetchData"
item-value="code"
class="elevation-1 data-table-wrapper"
:header-props="{ class: 'custom-header' }"
>
<template #header.checkbox> <template #header.checkbox>
<v-checkbox <v-checkbox :model-value="isAllSelected" @change="toggleSelectAll" hide-details density="compact" />
:model-value="isAllSelected"
@change="toggleSelectAll"
hide-details
density="compact"
/>
</template> </template>
<template #item.checkbox="{ item }"> <template #item.checkbox="{ item }">
<v-checkbox <v-checkbox :model-value="selectedItems.has(item.code)" @change="toggleSelection(item.code)" hide-details
:model-value="selectedItems.has(item.code)" density="compact" />
@change="toggleSelection(item.code)" </template>
hide-details
density="compact"
/>
</template>
<template #item.operation="{ item }"> <template #item.operation="{ item }">
<v-menu> <v-menu>
@ -163,10 +138,30 @@
</v-list> </v-list>
</v-menu> </v-menu>
</template> </template>
<template #item.amount="{ item }"> <template #item.amount="{ item }">
{{ $filters.formatNumber(item.amount) }} {{ $filters.formatNumber(item.amount) }}
</template> </template>
<template #item.costCenter="{ item }">
{{item.costCenters.map(center => center.name).join(', ') || '—'}}
</template>
</v-data-table-server> </v-data-table-server>
<v-row class="mt-4 pa-4">
<v-col cols="6">
<v-card flat>
<v-card-title>جمع کل هزینهها</v-card-title>
<v-card-text>{{ $filters.formatNumber(totalCost) }}</v-card-text>
</v-card>
</v-col>
<v-col cols="6">
<v-card flat>
<v-card-title>جمع موارد انتخابشده</v-card-title>
<v-card-text>{{ $filters.formatNumber(selectedCost) }}</v-card-text>
</v-card>
</v-col>
</v-row>
</template> </template>
<script setup> <script setup>
@ -175,8 +170,8 @@ import axios from 'axios';
import Swal from 'sweetalert2'; import Swal from 'sweetalert2';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { getApiUrl } from '/src/hesabixConfig'; import { getApiUrl } from '/src/hesabixConfig';
import moment from 'jalali-moment';
// تنظیم پایه URL از hesabixConfig
const apiUrl = getApiUrl(); const apiUrl = getApiUrl();
axios.defaults.baseURL = apiUrl; axios.defaults.baseURL = apiUrl;
@ -186,28 +181,26 @@ const items = ref([]);
const selectedItems = ref(new Set()); const selectedItems = ref(new Set());
const totalItems = ref(0); const totalItems = ref(0);
const searchQuery = ref(''); const searchQuery = ref('');
const timeFilter = ref('all');
// فیلترهای زمانی (بدون بازه دلخواه)
const timeFilters = ref([
{ label: 'امروز', value: 'today', checked: false },
{ label: 'این هفته', value: 'week', checked: false },
{ label: 'این ماه', value: 'month', checked: false },
{ label: 'همه', value: 'all', checked: true },
]);
// تعریف ستونهای جدول // تعریف ستونهای جدول
const headers = ref([ const headers = ref([
{ { title: '', key: 'checkbox', sortable: false, width: '50', align: 'center' },
title: '', { title: 'ردیف', key: 'index', align: 'center', sortable: false, width: '70' },
key: 'checkbox',
sortable: false,
width: '50',
align: 'center'
},
{
title: 'ردیف',
key: 'index',
align: 'center',
sortable: false,
width: '70'
},
{ title: 'عملیات', key: 'operation', align: 'center', sortable: false, width: '100' }, { title: 'عملیات', key: 'operation', align: 'center', sortable: false, width: '100' },
{ title: 'کد', key: 'code', align: 'center', sortable: true }, { title: 'کد', key: 'code', align: 'center', sortable: true },
{ title: 'مرکز هزینه', key: 'costCenter', align: 'center', sortable: false },
{ title: 'مبلغ', key: 'amount', align: 'center', sortable: true },
{ title: 'تاریخ', key: 'date', align: 'center', sortable: true }, { title: 'تاریخ', key: 'date', align: 'center', sortable: true },
{ title: 'شرح', key: 'des', align: 'center', sortable: true }, { title: 'شرح', key: 'des', align: 'center', sortable: true },
{ title: 'مبلغ', key: 'amount', align: 'center', sortable: true },
]); ]);
// تنظیمات سرور // تنظیمات سرور
@ -218,10 +211,20 @@ const serverOptions = ref({
sortDesc: [], sortDesc: [],
}); });
// اضافه کردن computed property برای کنترل وضعیت دکمههای عملیات // Computed properties
const hasSelected = computed(() => selectedItems.value.size > 0); const hasSelected = computed(() => selectedItems.value.size > 0);
const isAllSelected = computed(() => selectedItems.value.size === items.value.length); const isAllSelected = computed(() => selectedItems.value.size === items.value.length);
const totalCost = computed(() => {
return items.value.reduce((sum, item) => sum + Number(item.amount || 0), 0);
});
const selectedCost = computed(() => {
return items.value
.filter((item) => selectedItems.value.has(item.code))
.reduce((sum, item) => sum + Number(item.amount || 0), 0);
});
// فچ کردن دادهها از سرور // فچ کردن دادهها از سرور
const fetchData = async () => { const fetchData = async () => {
try { try {
@ -231,9 +234,33 @@ const fetchData = async () => {
if (searchQuery.value.trim()) { if (searchQuery.value.trim()) {
filters.search = { value: searchQuery.value.trim() }; filters.search = { value: searchQuery.value.trim() };
} }
if (timeFilter.value) {
filters.timeFilter = timeFilter.value;
const sortBy = serverOptions.value.sortBy?.[0] || 'code'; const today = moment().locale('fa').format('YYYY/MM/DD');
const sortDesc = serverOptions.value.sortDesc?.[0] ?? true; switch (timeFilter.value) {
case 'today':
filters.dateFrom = today;
filters.dateTo = today;
break;
case 'week':
filters.dateFrom = moment().locale('fa').subtract(6, 'days').format('YYYY/MM/DD');
filters.dateTo = today;
break;
case 'month':
filters.dateFrom = moment().locale('fa').startOf('jMonth').format('YYYY/MM/DD');
filters.dateTo = today;
break;
case 'all':
default:
break;
}
}
const sortByArray = Array.isArray(serverOptions.value.sortBy) ? serverOptions.value.sortBy : [];
const sortDescArray = Array.isArray(serverOptions.value.sortDesc) ? serverOptions.value.sortDesc : [];
const sortBy = sortByArray.length > 0 ? sortByArray[0].key : 'code';
const sortDesc = sortDescArray.length > 0 ? sortDescArray[0] : true;
const payload = { const payload = {
filters, filters,
@ -249,25 +276,23 @@ const fetchData = async () => {
const response = await axios.post('/api/cost/list/search', { const response = await axios.post('/api/cost/list/search', {
type: 'cost', type: 'cost',
...payload ...payload,
}); });
if (response.data?.items) { if (response.data?.items) {
// اضافه کردن شماره ردیف به هر آیتم
const startIndex = (serverOptions.value.page - 1) * serverOptions.value.itemsPerPage; const startIndex = (serverOptions.value.page - 1) * serverOptions.value.itemsPerPage;
items.value = response.data.items.map((item, index) => ({ items.value = response.data.items.map((item, index) => ({
...item, ...item,
index: startIndex + index + 1 index: startIndex + index + 1,
})); }));
totalItems.value = response.data.total; // استفاده از total از پاسخ سرور totalItems.value = response.data.total;
} else { } else {
items.value = []; items.value = [];
totalItems.value = 0; totalItems.value = 0;
} }
} catch (error) { } catch (error) {
console.error('Error fetching data:', error); console.error('Error fetching data:', error);
Swal.fire({ Swal.fire({
text: 'خطا در بارگذاری داده‌ها: ' + (error.response?.data?.detail || error.message), text: 'خطا در بارگذاری داده‌ها: ' + (error.response?.data?.detail || error.message),
icon: 'error', icon: 'error',
confirmButtonText: 'قبول', confirmButtonText: 'قبول',
@ -280,17 +305,26 @@ const fetchData = async () => {
// دیبونس برای جستجو // دیبونس برای جستجو
const debouncedSearch = debounce(() => fetchData(), 500); const debouncedSearch = debounce(() => fetchData(), 500);
// اعمال فیلتر زمانی
const applyTimeFilter = (value) => {
timeFilters.value.forEach((filter) => {
filter.checked = filter.value === value;
});
timeFilter.value = value;
fetchData();
};
// حذف یک آیتم // حذف یک آیتم
const deleteItem = async (code) => { const deleteItem = async (code) => {
const result = await Swal.fire({ const result = await Swal.fire({
text: 'آیا از حذف این آیتم اطمینان دارید؟', text: 'آیا از حذف این آیتم اطمینان دارید؟',
icon: 'warning', icon: 'warning',
showCancelButton: true, showCancelButton: true,
confirmButtonText: 'بله', confirmButtonText: 'بله',
cancelButtonText: 'خیر', cancelButtonText: 'خیر',
}); });
if (result.isConfirmed) { if (result.isConfirmed) {
try { try {
loading.value = true; loading.value = true;
const response = await axios.post('/api/accounting/remove', { code }); const response = await axios.post('/api/accounting/remove', { code });
@ -315,11 +349,8 @@ const deleteItem = async (code) => {
} }
}; };
// تابع toggleSelection را به این صورت تغییر میدهیم // انتخاب و لغو انتخاب
const toggleSelection = (code) => { const toggleSelection = (code) => {
const item = items.value.find(i => i.code === code);
if (!item) return;
if (selectedItems.value.has(code)) { if (selectedItems.value.has(code)) {
selectedItems.value.delete(code); selectedItems.value.delete(code);
} else { } else {
@ -327,18 +358,15 @@ const toggleSelection = (code) => {
} }
}; };
// تابع toggleSelectAll را به این صورت تغییر میدهیم
const toggleSelectAll = () => { const toggleSelectAll = () => {
if (selectedItems.value.size === items.value.length) { if (selectedItems.value.size === items.value.length) {
selectedItems.value.clear(); selectedItems.value.clear();
} else { } else {
items.value.forEach(item => { items.value.forEach((item) => selectedItems.value.add(item.code));
selectedItems.value.add(item.code);
});
} }
}; };
// تغییر توابع export // خروجی PDF
const exportPDF = async (all = false) => { const exportPDF = async (all = false) => {
try { try {
loading.value = true; loading.value = true;
@ -351,10 +379,9 @@ const exportPDF = async (all = false) => {
return; return;
} }
// ایجاد آرایهای از آیتمهای انتخاب شده const selectedItemsArray = all
const selectedItemsArray = all ? items.value
? items.value : items.value.filter((item) => selectedItems.value.has(item.code));
: items.value.filter(item => selectedItems.value.has(item.code));
const payload = all ? { all: true } : { items: selectedItemsArray }; const payload = all ? { all: true } : { items: selectedItemsArray };
const response = await axios.post('/api/costs/list/print', payload); const response = await axios.post('/api/costs/list/print', payload);
@ -372,6 +399,7 @@ const exportPDF = async (all = false) => {
} }
}; };
// خروجی Excel
const exportExcel = async (all = false) => { const exportExcel = async (all = false) => {
try { try {
loading.value = true; loading.value = true;
@ -384,19 +412,20 @@ const exportExcel = async (all = false) => {
return; return;
} }
// ایجاد آرایهای از آیتمهای انتخاب شده const selectedItemsArray = all
const selectedItemsArray = all ? items.value
? items.value : items.value.filter((item) => selectedItems.value.has(item.code));
: items.value.filter(item => selectedItems.value.has(item.code));
const payload = all ? { all: true } : { items: selectedItemsArray }; const payload = all ? { all: true } : { items: selectedItemsArray };
const response = await axios.post('/api/costs/list/excel', payload, { const response = await axios.post('/api/costs/list/excel', payload, {
responseType: 'blob' responseType: 'blob',
}); });
const url = window.URL.createObjectURL(new Blob([response.data], { const url = window.URL.createObjectURL(
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' new Blob([response.data], {
})); type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
);
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
link.setAttribute('download', 'costs.xlsx'); link.setAttribute('download', 'costs.xlsx');
@ -416,7 +445,7 @@ const exportExcel = async (all = false) => {
} }
}; };
// تغییر تابع deleteGroup // حذف گروهی
const deleteGroup = async () => { const deleteGroup = async () => {
if (selectedItems.value.size === 0) { if (selectedItems.value.size === 0) {
Swal.fire({ Swal.fire({
@ -439,18 +468,18 @@ const deleteGroup = async () => {
try { try {
loading.value = true; loading.value = true;
const selectedCodes = Array.from(selectedItems.value); const selectedCodes = Array.from(selectedItems.value);
const promises = selectedCodes.map(code => const promises = selectedCodes.map((code) =>
axios.post('/api/accounting/remove', { code }) axios.post('/api/accounting/remove', { code })
); );
await Promise.all(promises); await Promise.all(promises);
Swal.fire({ Swal.fire({
text: 'آیتم‌ها با موفقیت حذف شدند', text: 'آیتم‌ها با موفقیت حذف شدند',
icon: 'success', icon: 'success',
confirmButtonText: 'قبول', confirmButtonText: 'قبول',
}); });
selectedItems.value.clear(); selectedItems.value.clear();
fetchData(); fetchData();
} catch (error) { } catch (error) {
@ -466,10 +495,11 @@ const deleteGroup = async () => {
} }
}; };
// اضافه کردن watch برای پاک کردن انتخابها هنگام تغییر صفحه // Watchers
watch(() => serverOptions.value.page, () => { watch(() => serverOptions.value.page, () => {
selectedItems.value.clear(); selectedItems.value.clear();
}); });
watch(searchQuery, () => debouncedSearch());
// OnMounted // OnMounted
onMounted(() => { onMounted(() => {