almost done two step approval in persons part

This commit is contained in:
Hesabix 2025-09-03 03:13:49 +03:30
parent 3d454a642f
commit 1cef78c6f5
3 changed files with 486 additions and 52 deletions

View file

@ -1117,25 +1117,51 @@ class PersonsController extends AbstractController
} }
#[Route('/api/approval/approve/receive/{code}', name: 'app_approval_approve_receive', methods: ['POST'])] #[Route('/api/approval/approve/receive/{code}', name: 'app_approval_approve_receive', methods: ['POST'])]
public function approveReceiveDoc(string $code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse public function approveReceiveDoc($code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{ {
$acc = $access->hasRole('getpay'); $acc = $access->hasRole('getpay');
if (!$acc) throw $this->createAccessDeniedException(); if (!$acc) throw $this->createAccessDeniedException();
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([ // Debug: بررسی مقادیر
'bid' => $acc['bid']->getId(), $bidId = $acc['bid']->getId();
'code' => $code, $moneyId = $acc['money']->getId();
'money' => $acc['money']->getId(), $yearId = $acc['year']->getId();
'type' => 'person_receive'
]); // جستجوی سند با QueryBuilder برای debug بیشتر (بدون سال)
$queryBuilder = $entityManager->createQueryBuilder();
$doc = $queryBuilder
->select('d')
->from(HesabdariDoc::class, 'd')
->where('d.bid = :bid')
->andWhere('d.code = :code')
->andWhere('d.type = :type')
->setParameter('bid', $acc['bid'])
->setParameter('code', $code)
->setParameter('type', 'person_receive')
->getQuery()
->getOneOrNullResult();
if (!$doc) { if (!$doc) {
throw $this->createNotFoundException( // جستجوی تمام اسناد با همین code برای debug
'سند دریافت یافت نشد' $allDocsWithCode = $entityManager->createQueryBuilder()
. 'code ' . $code ->select('d')
. ' money ' . $acc['money']->getId() ->from(HesabdariDoc::class, 'd')
. ' year ' . $acc['year']->getId() ->where('d.code = :code')
. ' bid ' . $acc['bid']->getId() ->setParameter('code', $code)
); ->getQuery()
->getResult();
$debugInfo = 'سند دریافت یافت نشد - Debug Info: ';
$debugInfo .= 'code=' . $code . ', bid=' . $bidId . ', money=' . $moneyId . ', year=' . $yearId;
$debugInfo .= ', total docs with this code=' . count($allDocsWithCode);
foreach ($allDocsWithCode as $debugDoc) {
$debugInfo .= ', found doc: bid=' . $debugDoc->getBid()->getId() .
' money=' . $debugDoc->getMoney()->getId() .
' type=' . $debugDoc->getType();
}
throw $this->createNotFoundException($debugInfo);
} }
$doc->setIsPreview(false); $doc->setIsPreview(false);
@ -1148,15 +1174,14 @@ class PersonsController extends AbstractController
} }
#[Route('/api/approval/unapprove/receive/{code}', name: 'app_approval_unapprove_receive', methods: ['POST'])] #[Route('/api/approval/unapprove/receive/{code}', name: 'app_approval_unapprove_receive', methods: ['POST'])]
public function unapproveReceiveDoc(string $code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse public function unapproveReceiveDoc($code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{ {
$acc = $access->hasRole('getpay'); $acc = $access->hasRole('getpay');
if (!$acc) throw $this->createAccessDeniedException(); if (!$acc) throw $this->createAccessDeniedException();
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([ $doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid']->getId(), 'bid' => $acc['bid'],
'code' => $code, 'code' => $code,
'money' => $acc['money']->getId(),
'type' => 'person_receive' 'type' => 'person_receive'
]); ]);
if (!$doc) throw $this->createNotFoundException('سند دریافت یافت نشد'); if (!$doc) throw $this->createNotFoundException('سند دریافت یافت نشد');
@ -1183,14 +1208,42 @@ class PersonsController extends AbstractController
return $this->json(['success' => false, 'message' => 'هیچ سندی انتخاب نشده است']); return $this->json(['success' => false, 'message' => 'هیچ سندی انتخاب نشده است']);
} }
$docs = $entityManager->getRepository(HesabdariDoc::class)->findBy([ // جستجوی اسناد با QueryBuilder برای debug بهتر
'bid' => $acc['bid']->getId(), $queryBuilder = $entityManager->createQueryBuilder();
'code' => $docIds, $docs = $queryBuilder
'money' => $acc['money']->getId(), ->select('d')
'type' => 'person_receive' ->from(HesabdariDoc::class, 'd')
]); ->where('d.bid = :bid')
->andWhere('d.code IN (:codes)')
->andWhere('d.type = :type')
->setParameter('bid', $acc['bid'])
->setParameter('codes', $docIds)
->setParameter('type', 'person_receive')
->getQuery()
->getResult();
if (empty($docs)) { if (empty($docs)) {
return $this->json(['success' => false, 'message' => 'سند دریافت یافت نشد']); // Debug: جستجوی تمام اسناد با این codes
$allDocsWithCodes = $entityManager->createQueryBuilder()
->select('d')
->from(HesabdariDoc::class, 'd')
->where('d.code IN (:codes)')
->setParameter('codes', $docIds)
->getQuery()
->getResult();
$debugInfo = 'هیچ سند دریافت یافت نشد - Debug Info: ';
$debugInfo .= 'requested codes=' . implode(',', $docIds);
$debugInfo .= ', bid=' . $acc['bid']->getId();
$debugInfo .= ', total docs with these codes=' . count($allDocsWithCodes);
foreach ($allDocsWithCodes as $debugDoc) {
$debugInfo .= ', found doc: code=' . $debugDoc->getCode() .
' bid=' . $debugDoc->getBid()->getId() .
' type=' . $debugDoc->getType();
}
return $this->json(['success' => false, 'message' => $debugInfo]);
} }
foreach ($docs as $doc) { foreach ($docs as $doc) {
$doc->setIsPreview(false); $doc->setIsPreview(false);
@ -2430,6 +2483,7 @@ class PersonsController extends AbstractController
$itemsPerPage = (int) ($params['itemsPerPage'] ?? 10); $itemsPerPage = (int) ($params['itemsPerPage'] ?? 10);
$search = $params['search'] ?? ''; $search = $params['search'] ?? '';
$dateFilter = $params['dateFilter'] ?? 'all'; $dateFilter = $params['dateFilter'] ?? 'all';
$approvalFilter = $params['approvalFilter'] ?? 'all';
// پردازش پارامترهای سورت // پردازش پارامترهای سورت
$sortBy = 'id'; $sortBy = 'id';
@ -2454,16 +2508,30 @@ class PersonsController extends AbstractController
$queryBuilder = $entityManager->getRepository(HesabdariDoc::class) $queryBuilder = $entityManager->getRepository(HesabdariDoc::class)
->createQueryBuilder('d') ->createQueryBuilder('d')
->select('DISTINCT d.id, d.date, d.code, d.des, d.amount') ->select('DISTINCT d.id, d.date, d.code, d.des, d.amount')
->addSelect('d.isPreview, d.isApproved')
->addSelect('approver.fullName as approvedByName, approver.id as approvedById, approver.email as approvedByEmail')
->leftJoin('d.approvedBy', 'approver')
->where('d.bid = :bid') ->where('d.bid = :bid')
->andWhere('d.type = :type') ->andWhere('d.type = :type')
->andWhere('d.year = :year') ->andWhere('d.year = :year')
->andWhere('d.money = :money') ->andWhere('d.money = :money')
->andWhere('d.isApproved = :isApproved')
->setParameter('bid', $acc['bid']) ->setParameter('bid', $acc['bid'])
->setParameter('type', 'person_send') ->setParameter('type', 'person_send')
->setParameter('year', $acc['year']) ->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']) ->setParameter('money', $acc['money']);
->setParameter('isApproved', true);
// فیلتر تایید
if ($approvalFilter === 'approved') {
$queryBuilder->andWhere('d.isApproved = true');
} elseif ($approvalFilter === 'pending') {
$queryBuilder->andWhere('d.isPreview = true AND d.isApproved = false');
} else {
// اگر تایید دو مرحله‌ای فعال نیست، فقط اسناد تایید شده را نمایش بده
$business = $entityManager->getRepository(Business::class)->find($acc['bid']);
if (!$business || !$business->isRequireTwoStepApproval()) {
$queryBuilder->andWhere('d.isApproved = true');
}
}
// جست‌وجو // جست‌وجو
if (!empty($search)) { if (!empty($search)) {
@ -2514,12 +2582,23 @@ class PersonsController extends AbstractController
->andWhere('d.type = :type') ->andWhere('d.type = :type')
->andWhere('d.year = :year') ->andWhere('d.year = :year')
->andWhere('d.money = :money') ->andWhere('d.money = :money')
->andWhere('d.isApproved = :isApproved')
->setParameter('bid', $acc['bid']) ->setParameter('bid', $acc['bid'])
->setParameter('type', 'person_send') ->setParameter('type', 'person_send')
->setParameter('year', $acc['year']) ->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']) ->setParameter('money', $acc['money']);
->setParameter('isApproved', true);
// فیلتر تایید برای کوئری تعداد کل
if ($approvalFilter === 'approved') {
$totalQueryBuilder->andWhere('d.isApproved = true');
} elseif ($approvalFilter === 'pending') {
$totalQueryBuilder->andWhere('d.isPreview = true AND d.isApproved = false');
} else {
// اگر تایید دو مرحله‌ای فعال نیست، فقط اسناد تایید شده را نمایش بده
$business = $entityManager->getRepository(Business::class)->find($acc['bid']);
if (!$business || !$business->isRequireTwoStepApproval()) {
$totalQueryBuilder->andWhere('d.isApproved = true');
}
}
// اعمال فیلترهای جست‌وجو و تاریخ برای کوئری تعداد // اعمال فیلترهای جست‌وجو و تاریخ برای کوئری تعداد
if (!empty($search)) { if (!empty($search)) {
@ -2685,6 +2764,13 @@ class PersonsController extends AbstractController
'amount' => $doc['amount'], 'amount' => $doc['amount'],
'persons' => $persons[$doc['id']] ?? [], 'persons' => $persons[$doc['id']] ?? [],
'accounts' => $accounts[$doc['id']] ?? [], 'accounts' => $accounts[$doc['id']] ?? [],
'isPreview' => $doc['isPreview'],
'isApproved' => $doc['isApproved'],
'approvedBy' => $doc['approvedByName'] ? [
'fullName' => $doc['approvedByName'],
'id' => $doc['approvedById'],
'email' => $doc['approvedByEmail']
] : null,
]; ];
} }
@ -2694,4 +2780,150 @@ class PersonsController extends AbstractController
]); ]);
} }
#[Route('/api/approval/approve/send/{code}', name: 'app_approval_approve_send', methods: ['POST'])]
public function approveSendDoc($code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('getpay');
if (!$acc) throw $this->createAccessDeniedException();
// Debug: بررسی مقادیر
$bidId = $acc['bid']->getId();
$moneyId = $acc['money']->getId();
$yearId = $acc['year']->getId();
// جستجوی سند با QueryBuilder برای debug بیشتر (بدون سال)
$queryBuilder = $entityManager->createQueryBuilder();
$doc = $queryBuilder
->select('d')
->from(HesabdariDoc::class, 'd')
->where('d.bid = :bid')
->andWhere('d.code = :code')
->andWhere('d.type = :type')
->setParameter('bid', $acc['bid'])
->setParameter('code', $code)
->setParameter('type', 'person_send')
->getQuery()
->getOneOrNullResult();
if (!$doc) {
// جستجوی تمام اسناد با همین code برای debug
$allDocsWithCode = $entityManager->createQueryBuilder()
->select('d')
->from(HesabdariDoc::class, 'd')
->where('d.code = :code')
->setParameter('code', $code)
->getQuery()
->getResult();
$debugInfo = 'سند پرداخت یافت نشد - Debug Info: ';
$debugInfo .= 'code=' . $code . ', bid=' . $bidId . ', money=' . $moneyId . ', year=' . $yearId;
$debugInfo .= ', total docs with this code=' . count($allDocsWithCode);
foreach ($allDocsWithCode as $debugDoc) {
$debugInfo .= ', found doc: bid=' . $debugDoc->getBid()->getId() .
' money=' . $debugDoc->getMoney()->getId() .
' type=' . $debugDoc->getType();
}
throw $this->createNotFoundException($debugInfo);
}
$doc->setIsPreview(false);
$doc->setIsApproved(true);
$doc->setApprovedBy($this->getUser());
$entityManager->persist($doc);
$entityManager->flush();
return $this->json(['success' => true, 'message' => 'سند پرداخت تایید شد']);
}
#[Route('/api/approval/unapprove/send/{code}', name: 'app_approval_unapprove_send', methods: ['POST'])]
public function unapproveSendDoc($code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('getpay');
if (!$acc) throw $this->createAccessDeniedException();
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code,
'type' => 'person_send'
]);
if (!$doc) throw $this->createNotFoundException('سند پرداخت یافت نشد');
$doc->setIsPreview(true);
$doc->setIsApproved(false);
$doc->setApprovedBy(null);
$entityManager->persist($doc);
$entityManager->flush();
return $this->json(['success' => true, 'message' => 'تایید سند پرداخت لغو شد']);
}
#[Route('/api/approval/approve/group/send', name: 'app_approval_approve_group_send', methods: ['POST'])]
public function approveGroupSendDocs(Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('getpay');
if (!$acc) throw $this->createAccessDeniedException();
$params = json_decode($request->getContent(), true) ?? [];
$docIds = $params['docIds'] ?? [];
if (empty($docIds)) {
return $this->json(['success' => false, 'message' => 'هیچ سندی انتخاب نشده است']);
}
// جستجوی اسناد با QueryBuilder برای debug بهتر
$queryBuilder = $entityManager->createQueryBuilder();
$docs = $queryBuilder
->select('d')
->from(HesabdariDoc::class, 'd')
->where('d.bid = :bid')
->andWhere('d.code IN (:codes)')
->andWhere('d.type = :type')
->setParameter('bid', $acc['bid'])
->setParameter('codes', $docIds)
->setParameter('type', 'person_send')
->getQuery()
->getResult();
if (empty($docs)) {
// Debug: جستجوی تمام اسناد برای بررسی مشکل
$debugQuery = $entityManager->createQueryBuilder()
->select('d.code, d.type, d.bid')
->from(HesabdariDoc::class, 'd')
->where('d.code IN (:codes)')
->setParameter('codes', $docIds)
->getQuery()
->getArrayResult();
$debugInfo = 'اسناد پرداخت یافت نشد - Debug Info: ';
$debugInfo .= 'requested codes=' . implode(',', $docIds) . ', bid=' . $acc['bid']->getId();
$debugInfo .= ', found docs=' . count($debugQuery);
foreach ($debugQuery as $debugDoc) {
$debugInfo .= ', code=' . $debugDoc['code'] . ' type=' . $debugDoc['type'] . ' bid=' . $debugDoc['bid'];
}
return $this->json(['success' => false, 'message' => $debugInfo]);
}
$successCount = 0;
foreach ($docs as $doc) {
if ($doc->isPreview() && !$doc->isApproved()) {
$doc->setIsPreview(false);
$doc->setIsApproved(true);
$doc->setApprovedBy($this->getUser());
$entityManager->persist($doc);
$successCount++;
}
}
if ($successCount > 0) {
$entityManager->flush();
}
return $this->json(['success' => true, 'message' => "$successCount سند پرداخت تایید شد"]);
}
} }

View file

@ -14,12 +14,6 @@
<v-btn v-bind="props" icon="mdi-plus" color="primary" to="/acc/persons/receive/mod/"></v-btn> <v-btn v-bind="props" icon="mdi-plus" color="primary" to="/acc/persons/receive/mod/"></v-btn>
</template> </template>
</v-tooltip> </v-tooltip>
<v-tooltip v-if="checkApprover()" :text="'تایید اسناد انتخابی'" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-check-decagram" color="success" @click="approveSelectedReceives"
:disabled="selectedItems.length === 0" :loading="bulkLoading"></v-btn>
</template>
</v-tooltip>
<v-menu> <v-menu>
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" icon color="error"> <v-btn v-bind="props" icon color="error">
@ -74,6 +68,12 @@
<v-btn v-bind="props" icon="mdi-table-cog" color="primary" @click="showColumnDialog = true"></v-btn> <v-btn v-bind="props" icon="mdi-table-cog" color="primary" @click="showColumnDialog = true"></v-btn>
</template> </template>
</v-tooltip> </v-tooltip>
<v-tooltip v-if="checkApprover()" :text="'تایید اسناد انتخابی'" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-check-decagram" color="success" @click="approveSelectedReceives"
:disabled="selectedItems.length === 0" :loading="bulkLoading"></v-btn>
</template>
</v-tooltip>
</v-toolbar> </v-toolbar>
<!-- Tabs for two-step approval --> <!-- Tabs for two-step approval -->
<div v-if="business.requireTwoStepApproval" class="px-2 pt-2"> <div v-if="business.requireTwoStepApproval" class="px-2 pt-2">
@ -698,39 +698,43 @@ const canShowUnapproveButton = (item) => {
const approveReceive = async (code) => { const approveReceive = async (code) => {
try { try {
loading.value = true;
const response = await axios.post(`/api/approval/approve/receive/${code}`); const response = await axios.post(`/api/approval/approve/receive/${code}`);
await loadData();
if (response.data.success) { if (response.data.success) {
// پاک کردن انتخابها و بهروزرسانی فوری جدول
selectedItems.value = selectedItems.value.filter(item => item.code !== code);
selectAll.value = false;
updateSelectedSum();
console.log('Before loadData in approveReceive');
await loadData();
console.log('After loadData in approveReceive');
Swal.fire({ text: 'سند دریافت تایید شد', icon: 'success', confirmButtonText: 'قبول' }); Swal.fire({ text: 'سند دریافت تایید شد', icon: 'success', confirmButtonText: 'قبول' });
} else { } else {
Swal.fire({ text: response.data.message, icon: 'error', confirmButtonText: 'قبول' }); Swal.fire({ text: response.data.message, icon: 'error', confirmButtonText: 'قبول' });
} }
} catch (error) { } catch (error) {
Swal.fire({ text: 'خطا در تایید سند: ' + (error.response?.data?.message || error.message), icon: 'error', confirmButtonText: 'قبول' }); Swal.fire({ text: 'خطا در تایید سند: ' + (error.response?.data?.message || error.message), icon: 'error', confirmButtonText: 'قبول' });
} finally {
loading.value = false;
} }
}; };
const unapproveReceive = async (code) => { const unapproveReceive = async (code) => {
try { try {
loading.value = true;
const response = await axios.post(`/api/approval/unapprove/receive/${code}`); const response = await axios.post(`/api/approval/unapprove/receive/${code}`);
await loadData();
if (response.data.success) { if (response.data.success) {
// پاک کردن انتخابها و بهروزرسانی فوری جدول
selectedItems.value = selectedItems.value.filter(item => item.code !== code);
selectAll.value = false;
updateSelectedSum();
console.log('Before loadData in unapproveReceive');
await loadData();
console.log('After loadData in unapproveReceive');
Swal.fire({ text: 'تایید سند لغو شد', icon: 'success', confirmButtonText: 'قبول' }); Swal.fire({ text: 'تایید سند لغو شد', icon: 'success', confirmButtonText: 'قبول' });
} else { } else {
Swal.fire({ text: response.data.message, icon: 'error', confirmButtonText: 'قبول' }); Swal.fire({ text: response.data.message, icon: 'error', confirmButtonText: 'قبول' });
} }
} catch (error) { } catch (error) {
Swal.fire({ text: 'خطا در لغو تایید سند: ' + (error.response?.data?.message || error.message), icon: 'error', confirmButtonText: 'قبول' }); Swal.fire({ text: 'خطا در لغو تایید سند: ' + (error.response?.data?.message || error.message), icon: 'error', confirmButtonText: 'قبول' });
} finally {
loading.value = false;
} }
}; };
@ -758,14 +762,22 @@ const approveSelectedReceives = async () => {
bulkLoading.value = true; bulkLoading.value = true;
try { try {
await axios.post(`/api/approval/approve/group/receive`, { const response = await axios.post(`/api/approval/approve/group/receive`, {
'docIds': selectedItems.value.map(item => item.code) 'docIds': selectedItems.value.map(item => item.code)
}); });
if (response.data.success) {
Swal.fire({ text: 'اسناد تایید شدند.', icon: 'success', confirmButtonText: 'قبول' }); Swal.fire({ text: 'اسناد تایید شدند.', icon: 'success', confirmButtonText: 'قبول' });
selectedItems.value = []; selectedItems.value = [];
selectAll.value = false;
updateSelectedSum();
// بهروزرسانی فوری جدول
await loadData(); await loadData();
} else {
Swal.fire({ text: response.data.message || 'خطا در تایید اسناد', icon: 'error', confirmButtonText: 'قبول' });
}
} catch (e) { } catch (e) {
Swal.fire({ text: 'خطا در تایید اسناد', icon: 'error', confirmButtonText: 'قبول' }); Swal.fire({ text: 'خطا در تایید اسناد: ' + (e.response?.data?.message || e.message), icon: 'error', confirmButtonText: 'قبول' });
} finally { } finally {
bulkLoading.value = false; bulkLoading.value = false;
} }

View file

@ -61,7 +61,20 @@
<v-btn v-bind="props" icon="mdi-table-cog" color="primary" @click="showColumnDialog = true"></v-btn> <v-btn v-bind="props" icon="mdi-table-cog" color="primary" @click="showColumnDialog = true"></v-btn>
</template> </template>
</v-tooltip> </v-tooltip>
<v-tooltip v-if="checkApprover()" :text="'تایید اسناد انتخابی'" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-check-decagram" color="success" @click="approveSelectedSends"
:disabled="selectedItems.length === 0" :loading="bulkLoading"></v-btn>
</template>
</v-tooltip>
</v-toolbar> </v-toolbar>
<!-- Tabs for two-step approval -->
<div v-if="business.requireTwoStepApproval" class="px-2 pt-2">
<v-tabs v-model="currentTab" color="primary" density="comfortable" grow>
<v-tab value="approved">اسناد تایید شده</v-tab>
<v-tab value="pending">اسناد در انتظار تایید</v-tab>
</v-tabs>
</div>
<v-text-field <v-text-field
hide-details hide-details
color="green" color="green"
@ -154,6 +167,18 @@
</template> </template>
<v-list-item-title>سند حسابداری</v-list-item-title> <v-list-item-title>سند حسابداری</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item v-if="canShowApprovalButton(item)" title="تایید سند"
@click="approveSend(item.code)">
<template v-slot:prepend>
<v-icon color="success">mdi-check-decagram</v-icon>
</template>
</v-list-item>
<v-list-item v-if="canShowUnapproveButton(item)" title="لغو تایید سند"
@click="unapproveSend(item.code)">
<template v-slot:prepend>
<v-icon color="red">mdi-cancel</v-icon>
</template>
</v-list-item>
<v-list-item :to="{ name: 'person_send_mod', params: { id: item.code }}"> <v-list-item :to="{ name: 'person_send_mod', params: { id: item.code }}">
<template v-slot:prepend> <template v-slot:prepend>
<v-icon icon="mdi-pencil"></v-icon> <v-icon icon="mdi-pencil"></v-icon>
@ -198,6 +223,14 @@
<template v-slot:item.amount="{ item }"> <template v-slot:item.amount="{ item }">
<span class="text-left">{{ $filters.formatNumber(item.amount) }}</span> <span class="text-left">{{ $filters.formatNumber(item.amount) }}</span>
</template> </template>
<template v-slot:item.approvalStatus="{ item }">
<v-chip size="small" :color="getApprovalStatusColor(item)">
{{ getApprovalStatusText(item) }}
</v-chip>
</template>
<template v-slot:item.approvedBy="{ item }">
{{ item.approvedBy?.fullName || '-' }}
</template>
</v-data-table-server> </v-data-table-server>
<v-card class="my-4"> <v-card class="my-4">
<v-card-text> <v-card-text>
@ -278,6 +311,10 @@ const sumTotal = ref(0);
const sumSelected = ref(0); const sumSelected = ref(0);
const sortBy = ref('id'); const sortBy = ref('id');
const sortDesc = ref(true); const sortDesc = ref(true);
const currentTab = ref('approved');
const business = ref({ requireTwoStepApproval: false, approvers: { sendToPersons: null } });
const currentUser = ref({ email: '', owner: false });
const bulkLoading = ref(false);
const allHeaders = reactive([ const allHeaders = reactive([
{ title: '', key: 'select', sortable: false, visible: true, customizable: false }, { title: '', key: 'select', sortable: false, visible: true, customizable: false },
@ -288,13 +325,20 @@ const allHeaders = reactive([
{ title: 'تاریخ', key: 'date', sortable: true, visible: true }, { title: 'تاریخ', key: 'date', sortable: true, visible: true },
{ title: 'شرح', key: 'des', sortable: true, visible: true }, { title: 'شرح', key: 'des', sortable: true, visible: true },
{ title: 'مبلغ', key: 'amount', sortable: true, visible: true }, { title: 'مبلغ', key: 'amount', sortable: true, visible: true },
{ title: 'وضعیت تایید', key: 'approvalStatus', sortable: true, visible: true },
{ title: 'تاییدکننده', key: 'approvedBy', sortable: true, visible: true },
]); ]);
const customizableHeaders = computed(() => const customizableHeaders = computed(() =>
allHeaders.filter(h => h.customizable !== false && h.key !== 'operation') allHeaders.filter(h => h.customizable !== false && h.key !== 'operation')
); );
const visibleHeaders = computed(() => const visibleHeaders = computed(() =>
allHeaders.filter(h => h.customizable === false || h.visible) allHeaders.filter(h => {
if ((h.key === 'approvalStatus' || h.key === 'approvedBy') && !business.value.requireTwoStepApproval) {
return false;
}
return h.customizable === false || h.visible;
})
); );
const dateFilterOptions = [ const dateFilterOptions = [
@ -338,6 +382,7 @@ const loadData = async (options = null) => {
itemsPerPage: options?.itemsPerPage || itemsPerPage.value, itemsPerPage: options?.itemsPerPage || itemsPerPage.value,
search: searchValue.value, search: searchValue.value,
dateFilter: dateFilter.value, dateFilter: dateFilter.value,
approvalFilter: business.value.requireTwoStepApproval ? currentTab.value : 'all',
sortBy: [{ sortBy: [{
key: sortBy.value, key: sortBy.value,
order: sortDesc.value ? 'desc' : 'asc' order: sortDesc.value ? 'desc' : 'asc'
@ -587,8 +632,153 @@ watch([page, itemsPerPage], () => {
loadData(); loadData();
}, { deep: true }); }, { deep: true });
watch(currentTab, () => {
loadData();
});
// Methods for approval functionality
const checkApprover = () => {
return business.value.requireTwoStepApproval && (business.value.approvers.sendToPersons == currentUser.value.email || currentUser.value.owner === true);
};
const getApprovalStatusText = (item) => {
if (!business.value?.requireTwoStepApproval) return 'تایید دو مرحله‌ای غیرفعال';
if (item.isPreview) return 'در انتظار تایید';
if (item.isApproved) return 'تایید شده';
return 'تایید شده';
};
const getApprovalStatusColor = (item) => {
if (!business.value?.requireTwoStepApproval) return 'default';
if (item.isPreview) return 'warning';
if (item.isApproved) return 'success';
return 'success';
};
const canShowApprovalButton = (item) => {
if (!checkApprover()) return false;
if (item?.isApproved) return false;
return true;
};
const canShowUnapproveButton = (item) => {
return !canShowApprovalButton(item) && checkApprover();
};
const approveSend = async (code) => {
try {
const response = await axios.post(`/api/approval/approve/send/${code}`);
if (response.data.success) {
// پاک کردن انتخابها و بهروزرسانی فوری جدول
selectedItems.value = selectedItems.value.filter(item => item.code !== code);
selectAll.value = false;
updateSelectedSum();
console.log('Before loadData in approveSend');
await loadData();
console.log('After loadData in approveSend');
Swal.fire({ text: 'سند پرداخت تایید شد', icon: 'success', confirmButtonText: 'قبول' });
} else {
Swal.fire({ text: response.data.message, icon: 'error', confirmButtonText: 'قبول' });
}
} catch (error) {
Swal.fire({ text: 'خطا در تایید سند: ' + (error.response?.data?.message || error.message), icon: 'error', confirmButtonText: 'قبول' });
}
};
const unapproveSend = async (code) => {
try {
const response = await axios.post(`/api/approval/unapprove/send/${code}`);
if (response.data.success) {
// پاک کردن انتخابها و بهروزرسانی فوری جدول
selectedItems.value = selectedItems.value.filter(item => item.code !== code);
selectAll.value = false;
updateSelectedSum();
console.log('Before loadData in unapproveSend');
await loadData();
console.log('After loadData in unapproveSend');
Swal.fire({ text: 'تایید سند لغو شد', icon: 'success', confirmButtonText: 'قبول' });
} else {
Swal.fire({ text: response.data.message, icon: 'error', confirmButtonText: 'قبول' });
}
} catch (error) {
Swal.fire({ text: 'خطا در لغو تایید سند: ' + (error.response?.data?.message || error.message), icon: 'error', confirmButtonText: 'قبول' });
}
};
const approveSelectedSends = async () => {
if (selectedItems.value.length === 0) {
Swal.fire({ text: 'هیچ موردی انتخاب نشده است.', icon: 'warning', confirmButtonText: 'قبول' });
return;
}
const selectedSends = items.value.filter(send => selectedItems.value.some(sel => sel.code === send.code));
if (selectedSends.some(send => !(!send.isApproved && send.isPreview))) {
Swal.fire({ text: 'برخی اسناد انتخابی تایید شده هستند.', icon: 'warning', confirmButtonText: 'قبول' });
return;
}
Swal.fire({
title: 'تایید اسناد انتخابی',
text: 'اسناد انتخاب‌شده تایید خواهند شد.',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'بله',
cancelButtonText: 'خیر'
}).then(async (r) => {
if (!r.isConfirmed) return;
bulkLoading.value = true;
try {
const response = await axios.post(`/api/approval/approve/group/send`, {
'docIds': selectedItems.value.map(item => item.code)
});
if (response.data.success) {
Swal.fire({ text: 'اسناد تایید شدند.', icon: 'success', confirmButtonText: 'قبول' });
selectedItems.value = [];
selectAll.value = false;
updateSelectedSum();
// بهروزرسانی فوری جدول
await loadData();
} else {
Swal.fire({ text: response.data.message || 'خطا در تایید اسناد', icon: 'error', confirmButtonText: 'قبول' });
}
} catch (e) {
Swal.fire({ text: 'خطا در تایید اسناد: ' + (e.response?.data?.message || e.message), icon: 'error', confirmButtonText: 'قبول' });
} finally {
bulkLoading.value = false;
}
});
};
const loadBusinessInfo = async () => {
try {
const response = await axios.get('/api/business/get/info/' + localStorage.getItem('activeBid'));
business.value = response.data || { requireTwoStepApproval: false, approvers: { sendToPersons: null } };
} catch (error) {
console.error('Error loading business info:', error);
business.value = { requireTwoStepApproval: false, approvers: { sendToPersons: null } };
}
};
const loadCurrentUser = async () => {
try {
const response = await axios.post('/api/business/get/user/permissions');
currentUser.value = response.data || { email: '', owner: false };
} catch (error) {
console.error('Error loading current user:', error);
currentUser.value = { email: '', owner: false };
}
};
onMounted(() => { onMounted(() => {
loadColumnSettings(); loadColumnSettings();
loadBusinessInfo();
loadCurrentUser();
loadData(); loadData();
}); });
</script> </script>