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

View file

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

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>
<v-menu v-model="isMenuOpen" :close-on-content-click="false">
<template v-slot:activator="{ props }">
<v-text-field
v-model="output"
readonly
v-bind="props"
:value="output"
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>
<div>
<v-text-field v-model="displayDate" :label="label" prepend-inner-icon="mdi-calendar" persistent-placeholder
class="v-date-input" :rules="rules" @input="updateDateFromInput" @click:prepend="togglePicker"></v-text-field>
<date-picker v-model="displayDate" type="date" format="jYYYY/jMM/jDD" display-format="jYYYY/jMM/jDD"
:min="minDatePersian" :max="maxDatePersian" custom-input=".v-date-input" :input-mode="false"
:editable="pickerActive" @close="pickerActive = false"></date-picker>
</div>
</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>

View file

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

View file

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

View file

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

View file

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

View file

@ -1,238 +1,251 @@
<template>
<v-container>
<v-form @submit.prevent="submitForm">
<v-row>
<v-col cols="12" md="6">
<v-text-field
v-model="form.date"
label="تاریخ (شمسی)"
placeholder="1403/02/28"
:rules="[v => !!v || 'تاریخ الزامی است']"
></v-text-field>
</v-col>
</v-row>
<v-container>
<v-form @submit.prevent="submitForm">
<v-row>
<v-col cols="12" md="6">
<Hdatepicker
v-model="form.date"
:rules="[v => !!v || 'تاریخ الزامی است']"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="form.des"
label="توضیحات سند"
placeholder="توضیحات مربوط به سند"
></v-text-field>
</v-col>
</v-row>
<v-data-table
:headers="headers"
:items="form.rows"
class="elevation-1"
hide-default-footer
>
<template v-slot:top>
<v-toolbar flat>
<v-toolbar-title>ردیفهای سند</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn color="primary" @click="addRow">افزودن ردیف</v-btn>
</v-toolbar>
</template>
<v-data-table
:headers="headers"
:items="form.rows"
class="elevation-1"
hide-default-footer
>
<template v-slot:top>
<v-toolbar flat>
<v-toolbar-title>ردیفهای سند</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn color="primary" @click="addRow">افزودن ردیف</v-btn>
</v-toolbar>
</template>
<template v-slot:item.ref="{ item }">
<v-menu offset-y>
<template v-slot:activator="{ props }">
<v-text-field
v-model="item.refName"
label="حساب"
dense
readonly
v-bind="props"
:rules="[v => !!item.ref || 'حساب الزامی است']"
></v-text-field>
<template v-slot:item.ref="{ item }">
<v-menu offset-y>
<template v-slot:activator="{ props }">
<v-text-field
v-model="item.refName"
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>
<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>
</v-treeview>
</v-menu>
</template>
</v-treeview>
</v-menu>
</template>
<template v-slot:item.bd="{ item }">
<v-text-field
v-model="item.bd"
label="بدهکار"
type="number"
dense
@input="calculateTotals"
></v-text-field>
</template>
<template v-slot:item.bd="{ item }">
<v-text-field
v-model="item.bd"
label="بدهکار"
type="number"
dense
@input="calculateTotals"
></v-text-field>
</template>
<template v-slot:item.bs="{ item }">
<v-text-field
v-model="item.bs"
label="بستانکار"
type="number"
dense
@input="calculateTotals"
></v-text-field>
</template>
<template v-slot:item.bs="{ item }">
<v-text-field
v-model="item.bs"
label="بستانکار"
type="number"
dense
@input="calculateTotals"
></v-text-field>
</template>
<template v-slot:item.des="{ item }">
<v-text-field v-model="item.des" label="توضیحات" dense></v-text-field>
</template>
<template v-slot:item.des="{ item }">
<v-text-field v-model="item.des" label="توضیحات" dense></v-text-field>
</template>
<template v-slot:item.actions="{ item }">
<v-btn color="error" small @click="removeRow(item)">حذف</v-btn>
</template>
</v-data-table>
<template v-slot:item.actions="{ item }">
<v-btn color="error" small @click="removeRow(item)">حذف</v-btn>
</template>
</v-data-table>
<v-row class="mt-4">
<v-col cols="6">
<v-text-field
:value="totalBd"
label="جمع بدهکار"
readonly
dense
></v-text-field>
</v-col>
<v-col cols="6">
<v-text-field
:value="totalBs"
label="جمع بستانکار"
readonly
dense
></v-text-field>
</v-col>
</v-row>
<v-row class="mt-4">
<v-col cols="6">
<v-text-field
:value="totalBd"
label="جمع بدهکار"
readonly
dense
></v-text-field>
</v-col>
<v-col cols="6">
<v-text-field
:value="totalBs"
label="جمع بستانکار"
readonly
dense
></v-text-field>
</v-col>
</v-row>
<v-alert v-if="error" type="error" class="mt-4">{{ error }}</v-alert>
<v-alert v-if="error" type="error" class="mt-4">{{ error }}</v-alert>
<v-btn type="submit" color="success" class="mt-4" :disabled="totalBd !== totalBs || !form.date">
ثبت سند
</v-btn>
</v-form>
</v-container>
</template>
<v-btn type="submit" color="success" class="mt-4" :disabled="totalBd !== totalBs || !form.date">
ثبت سند
</v-btn>
</v-form>
</v-container>
</template>
<script>
import axios from 'axios';
export default {
props: {
docId: {
type: Number,
default: null,
},
<script>
import axios from 'axios';
import moment from 'jalali-moment';
import Hdatepicker from '@/components/forms/Hdatepicker.vue';
export default {
components: {
Hdatepicker,
},
props: {
docId: {
type: Number,
default: null,
},
data() {
return {
form: {
date: '',
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 },
},
data() {
return {
form: {
date: '', // تاریخ به فرمت ISO (مثلاً 2025-03-24)
des: '',
rows: [
{ ref: null, refName: '', bd: '0', bs: '0', des: '', selectedAccounts: [] },
],
};
},
mounted() {
this.fetchHesabdariTables();
if (this.docId) {
this.fetchDoc();
},
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 },
],
};
},
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 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 || 'مشکل ناشناخته');
}
},
async fetchDoc() {
try {
const response = await axios.get(`/api/hesabdari/doc/${this.docId}`);
this.form.date = response.data.data.date;
this.form.rows = response.data.data.rows.map(row => ({
ref: row.ref.id,
refName: row.ref.name,
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 || 'خطا در ثبت سند';
}
},
async fetchDoc() {
try {
const response = await axios.get(`/api/hesabdari/doc/${this.docId}`);
const serverDate = response.data.data.date; // فرض: تاریخ شمسی از سرور
this.form.date = moment(serverDate, 'YYYY/MM/DD').format('YYYY-MM-DD');
this.form.des = response.data.data.des || '';
this.form.rows = response.data.data.rows.map(row => ({
ref: row.ref.id,
refName: row.ref.name,
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 || 'مشکل ناشناخته');
}
},
};
</script>
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;
}
<style scoped>
.v-data-table {
margin-top: 20px;
}
</style>
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>
<v-tooltip :text="$t('dialog.back')" location="bottom">
<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>
</v-tooltip>
</template>
<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-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-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-btn>
<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-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>
<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-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-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-btn>
<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-btn>
</template>
<v-list>
<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>
<v-list>
<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>
</v-list-item>
<v-list-item class="text-dark" :title="$t('dialog.all')" @click="exportExcel(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-list-item>
<v-list-item class="text-dark" :title="$t('dialog.all')" @click="exportExcel(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-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-slide-group-item>
</v-slide-group>
<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-text-field
:loading="loading"
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"
>
<v-text-field :loading="loading" color="green" class="mb-0 pt-0 rounded-0" hide-details="auto" density="compact"
:placeholder="$t('dialog.search_txt')" v-model="searchQuery" type="text" clearable>
<template v-slot:prepend-inner>
<v-tooltip location="bottom" :text="$t('dialog.search')">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" color="danger" icon="mdi-magnify" />
</template>
</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>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-text-field>
<v-data-table-server
:headers="headers"
:items="items"
: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' }"
>
<v-data-table-server :headers="headers" :items="items" :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>
<v-checkbox
:model-value="isAllSelected"
@change="toggleSelectAll"
hide-details
density="compact"
/>
<v-checkbox :model-value="isAllSelected" @change="toggleSelectAll" hide-details density="compact" />
</template>
<template #item.checkbox="{ item }">
<v-checkbox
:model-value="selectedItems.has(item.code)"
@change="toggleSelection(item.code)"
hide-details
density="compact"
/>
</template>
<v-checkbox :model-value="selectedItems.has(item.code)" @change="toggleSelection(item.code)" hide-details
density="compact" />
</template>
<template #item.operation="{ item }">
<v-menu>
@ -163,10 +138,30 @@
</v-list>
</v-menu>
</template>
<template #item.amount="{ item }">
{{ $filters.formatNumber(item.amount) }}
</template>
<template #item.costCenter="{ item }">
{{item.costCenters.map(center => center.name).join(', ') || '—'}}
</template>
</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>
<script setup>
@ -175,8 +170,8 @@ import axios from 'axios';
import Swal from 'sweetalert2';
import { debounce } from 'lodash';
import { getApiUrl } from '/src/hesabixConfig';
import moment from 'jalali-moment';
// تنظیم پایه URL از hesabixConfig
const apiUrl = getApiUrl();
axios.defaults.baseURL = apiUrl;
@ -186,28 +181,26 @@ const items = ref([]);
const selectedItems = ref(new Set());
const totalItems = ref(0);
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([
{
title: '',
key: 'checkbox',
sortable: false,
width: '50',
align: 'center'
},
{
title: 'ردیف',
key: 'index',
align: 'center',
sortable: false,
width: '70'
},
{ title: '', 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: '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: 'des', align: 'center', sortable: true },
{ title: 'مبلغ', key: 'amount', align: 'center', sortable: true },
]);
// تنظیمات سرور
@ -218,10 +211,20 @@ const serverOptions = ref({
sortDesc: [],
});
// اضافه کردن computed property برای کنترل وضعیت دکمههای عملیات
// Computed properties
const hasSelected = computed(() => selectedItems.value.size > 0);
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 () => {
try {
@ -231,9 +234,33 @@ const fetchData = async () => {
if (searchQuery.value.trim()) {
filters.search = { value: searchQuery.value.trim() };
}
if (timeFilter.value) {
filters.timeFilter = timeFilter.value;
const sortBy = serverOptions.value.sortBy?.[0] || 'code';
const sortDesc = serverOptions.value.sortDesc?.[0] ?? true;
const today = moment().locale('fa').format('YYYY/MM/DD');
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 = {
filters,
@ -249,25 +276,23 @@ const fetchData = async () => {
const response = await axios.post('/api/cost/list/search', {
type: 'cost',
...payload
...payload,
});
if (response.data?.items) {
// اضافه کردن شماره ردیف به هر آیتم
const startIndex = (serverOptions.value.page - 1) * serverOptions.value.itemsPerPage;
items.value = response.data.items.map((item, index) => ({
...item,
index: startIndex + index + 1
index: startIndex + index + 1,
}));
totalItems.value = response.data.total; // استفاده از total از پاسخ سرور
totalItems.value = response.data.total;
} else {
items.value = [];
totalItems.value = 0;
}
} catch (error) {
console.error('Error fetching data:', error);
Swal.fire({
Swal.fire({
text: 'خطا در بارگذاری داده‌ها: ' + (error.response?.data?.detail || error.message),
icon: 'error',
confirmButtonText: 'قبول',
@ -280,17 +305,26 @@ const fetchData = async () => {
// دیبونس برای جستجو
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 result = await Swal.fire({
text: 'آیا از حذف این آیتم اطمینان دارید؟',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'بله',
showCancelButton: true,
confirmButtonText: 'بله',
cancelButtonText: 'خیر',
});
if (result.isConfirmed) {
if (result.isConfirmed) {
try {
loading.value = true;
const response = await axios.post('/api/accounting/remove', { code });
@ -315,11 +349,8 @@ const deleteItem = async (code) => {
}
};
// تابع toggleSelection را به این صورت تغییر میدهیم
// انتخاب و لغو انتخاب
const toggleSelection = (code) => {
const item = items.value.find(i => i.code === code);
if (!item) return;
if (selectedItems.value.has(code)) {
selectedItems.value.delete(code);
} else {
@ -327,18 +358,15 @@ const toggleSelection = (code) => {
}
};
// تابع toggleSelectAll را به این صورت تغییر میدهیم
const toggleSelectAll = () => {
if (selectedItems.value.size === items.value.length) {
selectedItems.value.clear();
} else {
items.value.forEach(item => {
selectedItems.value.add(item.code);
});
items.value.forEach((item) => selectedItems.value.add(item.code));
}
};
// تغییر توابع export
// خروجی PDF
const exportPDF = async (all = false) => {
try {
loading.value = true;
@ -351,10 +379,9 @@ const exportPDF = async (all = false) => {
return;
}
// ایجاد آرایهای از آیتمهای انتخاب شده
const selectedItemsArray = all
? 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 response = await axios.post('/api/costs/list/print', payload);
@ -372,6 +399,7 @@ const exportPDF = async (all = false) => {
}
};
// خروجی Excel
const exportExcel = async (all = false) => {
try {
loading.value = true;
@ -384,19 +412,20 @@ const exportExcel = async (all = false) => {
return;
}
// ایجاد آرایهای از آیتمهای انتخاب شده
const selectedItemsArray = all
? 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 response = await axios.post('/api/costs/list/excel', payload, {
responseType: 'blob'
responseType: 'blob',
});
const url = window.URL.createObjectURL(new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
}));
const url = window.URL.createObjectURL(
new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'costs.xlsx');
@ -416,7 +445,7 @@ const exportExcel = async (all = false) => {
}
};
// تغییر تابع deleteGroup
// حذف گروهی
const deleteGroup = async () => {
if (selectedItems.value.size === 0) {
Swal.fire({
@ -439,7 +468,7 @@ const deleteGroup = async () => {
try {
loading.value = true;
const selectedCodes = Array.from(selectedItems.value);
const promises = selectedCodes.map(code =>
const promises = selectedCodes.map((code) =>
axios.post('/api/accounting/remove', { code })
);
@ -466,10 +495,11 @@ const deleteGroup = async () => {
}
};
// اضافه کردن watch برای پاک کردن انتخابها هنگام تغییر صفحه
// Watchers
watch(() => serverOptions.value.page, () => {
selectedItems.value.clear();
});
watch(searchQuery, () => debouncedSearch());
// OnMounted
onMounted(() => {