bug fix in cost controller list
This commit is contained in:
parent
dd101c8ee2
commit
65d45e73d1
|
@ -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++;
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
|
@ -1,4 +1,3 @@
|
|||
import moment from 'moment'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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(() => {
|
||||
|
|
Loading…
Reference in a new issue