progress in two step approval
This commit is contained in:
parent
822402cfda
commit
3d454a642f
|
|
@ -1116,6 +1116,94 @@ class PersonsController extends AbstractController
|
|||
]);
|
||||
}
|
||||
|
||||
#[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
|
||||
{
|
||||
$acc = $access->hasRole('getpay');
|
||||
if (!$acc) throw $this->createAccessDeniedException();
|
||||
|
||||
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
|
||||
'bid' => $acc['bid']->getId(),
|
||||
'code' => $code,
|
||||
'money' => $acc['money']->getId(),
|
||||
'type' => 'person_receive'
|
||||
]);
|
||||
if (!$doc) {
|
||||
throw $this->createNotFoundException(
|
||||
'سند دریافت یافت نشد'
|
||||
. 'code ' . $code
|
||||
. ' money ' . $acc['money']->getId()
|
||||
. ' year ' . $acc['year']->getId()
|
||||
. ' bid ' . $acc['bid']->getId()
|
||||
);
|
||||
}
|
||||
|
||||
$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/receive/{code}', name: 'app_approval_unapprove_receive', methods: ['POST'])]
|
||||
public function unapproveReceiveDoc(string $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']->getId(),
|
||||
'code' => $code,
|
||||
'money' => $acc['money']->getId(),
|
||||
'type' => 'person_receive'
|
||||
]);
|
||||
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/receive', name: 'app_approval_approve_group_receive', methods: ['POST'])]
|
||||
public function approveGroupReceiveDocs(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' => 'هیچ سندی انتخاب نشده است']);
|
||||
}
|
||||
|
||||
$docs = $entityManager->getRepository(HesabdariDoc::class)->findBy([
|
||||
'bid' => $acc['bid']->getId(),
|
||||
'code' => $docIds,
|
||||
'money' => $acc['money']->getId(),
|
||||
'type' => 'person_receive'
|
||||
]);
|
||||
if (empty($docs)) {
|
||||
return $this->json(['success' => false, 'message' => 'سند دریافت یافت نشد']);
|
||||
}
|
||||
foreach ($docs as $doc) {
|
||||
$doc->setIsPreview(false);
|
||||
$doc->setIsApproved(true);
|
||||
$doc->setApprovedBy($this->getUser());
|
||||
$entityManager->persist($doc);
|
||||
}
|
||||
|
||||
$entityManager->flush();
|
||||
|
||||
return $this->json(['success' => true, 'message' => 'اسناد دریافت تایید شدند']);
|
||||
}
|
||||
|
||||
#[Route('/api/person/receive/list/search', name: 'app_persons_receive_list_search', methods: ['POST'])]
|
||||
public function app_persons_receive_list_search(
|
||||
Request $request,
|
||||
|
|
@ -1134,6 +1222,7 @@ class PersonsController extends AbstractController
|
|||
$itemsPerPage = (int) ($params['itemsPerPage'] ?? 10);
|
||||
$search = $params['search'] ?? '';
|
||||
$dateFilter = $params['dateFilter'] ?? 'all';
|
||||
$approvalFilter = $params['approvalFilter'] ?? 'all'; // جدید: فیلتر وضعیت تایید
|
||||
|
||||
// پردازش پارامترهای سورت
|
||||
$sortBy = 'id';
|
||||
|
|
@ -1157,17 +1246,33 @@ class PersonsController extends AbstractController
|
|||
// کوئری پایه برای اسناد
|
||||
$queryBuilder = $entityManager->getRepository(HesabdariDoc::class)
|
||||
->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, d.isPreview, d.isApproved')
|
||||
->addSelect('u.fullName as submitter')
|
||||
->addSelect('approver.fullName as approvedByName, approver.id as approvedById, approver.email as approvedByEmail')
|
||||
->leftJoin('d.submitter', 'u')
|
||||
->leftJoin('d.approvedBy', 'approver')
|
||||
->where('d.bid = :bid')
|
||||
->andWhere('d.type = :type')
|
||||
->andWhere('d.year = :year')
|
||||
->andWhere('d.money = :money')
|
||||
->andWhere('d.isApproved = :isApproved')
|
||||
->setParameter('bid', $acc['bid'])
|
||||
->setParameter('type', 'person_receive')
|
||||
->setParameter('year', $acc['year'])
|
||||
->setParameter('money', $acc['money'])
|
||||
->setParameter('money', $acc['money']);
|
||||
|
||||
// اعمال فیلتر وضعیت تایید
|
||||
if ($approvalFilter === 'approved') {
|
||||
$queryBuilder->andWhere('d.isApproved = :isApproved')
|
||||
->setParameter('isApproved', true);
|
||||
} elseif ($approvalFilter === 'pending') {
|
||||
$queryBuilder->andWhere('d.isPreview = :isPreview AND d.isApproved = :isApproved')
|
||||
->setParameter('isPreview', true)
|
||||
->setParameter('isApproved', false);
|
||||
} else {
|
||||
// حالت قدیمی: فقط اسناد تایید شده
|
||||
$queryBuilder->andWhere('d.isApproved = :isApproved')
|
||||
->setParameter('isApproved', true);
|
||||
}
|
||||
|
||||
// جستوجو
|
||||
if (!empty($search)) {
|
||||
|
|
@ -1218,12 +1323,24 @@ class PersonsController extends AbstractController
|
|||
->andWhere('d.type = :type')
|
||||
->andWhere('d.year = :year')
|
||||
->andWhere('d.money = :money')
|
||||
->andWhere('d.isApproved = :isApproved')
|
||||
->setParameter('bid', $acc['bid'])
|
||||
->setParameter('type', 'person_receive')
|
||||
->setParameter('year', $acc['year'])
|
||||
->setParameter('money', $acc['money'])
|
||||
->setParameter('money', $acc['money']);
|
||||
|
||||
// اعمال فیلتر وضعیت تایید برای کوئری تعداد
|
||||
if ($approvalFilter === 'approved') {
|
||||
$totalQueryBuilder->andWhere('d.isApproved = :isApproved')
|
||||
->setParameter('isApproved', true);
|
||||
} elseif ($approvalFilter === 'pending') {
|
||||
$totalQueryBuilder->andWhere('d.isPreview = :isPreview AND d.isApproved = :isApproved')
|
||||
->setParameter('isPreview', true)
|
||||
->setParameter('isApproved', false);
|
||||
} else {
|
||||
// حالت قدیمی: فقط اسناد تایید شده
|
||||
$totalQueryBuilder->andWhere('d.isApproved = :isApproved')
|
||||
->setParameter('isApproved', true);
|
||||
}
|
||||
|
||||
// اعمال فیلترهای جستوجو و تاریخ برای کوئری تعداد
|
||||
if (!empty($search)) {
|
||||
|
|
@ -1389,6 +1506,14 @@ class PersonsController extends AbstractController
|
|||
'amount' => $doc['amount'],
|
||||
'persons' => $persons[$doc['id']] ?? [],
|
||||
'accounts' => $accounts[$doc['id']] ?? [],
|
||||
'isPreview' => $doc['isPreview'],
|
||||
'isApproved' => $doc['isApproved'],
|
||||
'submitter' => $doc['submitter'],
|
||||
'approvedBy' => $doc['approvedByName'] ? [
|
||||
'fullName' => $doc['approvedByName'],
|
||||
'id' => $doc['approvedById'],
|
||||
'email' => $doc['approvedByEmail']
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@
|
|||
<v-btn v-bind="props" icon="mdi-plus" color="primary" to="/acc/persons/receive/mod/"></v-btn>
|
||||
</template>
|
||||
</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>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon color="error">
|
||||
|
|
@ -69,6 +75,13 @@
|
|||
</template>
|
||||
</v-tooltip>
|
||||
</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
|
||||
hide-details
|
||||
color="green"
|
||||
|
|
@ -161,6 +174,18 @@
|
|||
</template>
|
||||
<v-list-item-title>سند حسابداری</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="canShowApprovalButton(item)" title="تایید سند"
|
||||
@click="approveReceive(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="unapproveReceive(item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="red">mdi-cancel</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item :to="{ name: 'person_receive_mod', params: { id: item.code }}">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-pencil"></v-icon>
|
||||
|
|
@ -205,6 +230,14 @@
|
|||
<template v-slot:item.amount="{ item }">
|
||||
<span class="text-left">{{ $filters.formatNumber(item.amount) }}</span>
|
||||
</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-card class="my-4">
|
||||
<v-card-text>
|
||||
|
|
@ -285,6 +318,10 @@ const sumTotal = ref(0);
|
|||
const sumSelected = ref(0);
|
||||
const sortBy = ref('id');
|
||||
const sortDesc = ref(true);
|
||||
const currentTab = ref('approved');
|
||||
const business = ref({ requireTwoStepApproval: false, approvers: { receiveFromPersons: null } });
|
||||
const currentUser = ref({ email: '', owner: false });
|
||||
const bulkLoading = ref(false);
|
||||
|
||||
const allHeaders = reactive([
|
||||
{ title: '', key: 'select', sortable: false, visible: true, customizable: false },
|
||||
|
|
@ -295,13 +332,20 @@ const allHeaders = reactive([
|
|||
{ title: 'تاریخ', key: 'date', sortable: true, visible: true },
|
||||
{ title: 'شرح', key: 'des', 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(() =>
|
||||
allHeaders.filter(h => h.customizable !== false && h.key !== 'operation')
|
||||
);
|
||||
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 = [
|
||||
|
|
@ -345,6 +389,7 @@ const loadData = async (options = null) => {
|
|||
itemsPerPage: options?.itemsPerPage || itemsPerPage.value,
|
||||
search: searchValue.value,
|
||||
dateFilter: dateFilter.value,
|
||||
approvalFilter: business.value.requireTwoStepApproval ? currentTab.value : 'all',
|
||||
sortBy: [{
|
||||
key: sortBy.value,
|
||||
order: sortDesc.value ? 'desc' : 'asc'
|
||||
|
|
@ -616,8 +661,141 @@ watch([page, itemsPerPage], () => {
|
|||
loadData();
|
||||
}, { deep: true });
|
||||
|
||||
watch(currentTab, () => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
// Methods for approval functionality
|
||||
const checkApprover = () => {
|
||||
return business.value.requireTwoStepApproval && (business.value.approvers.receiveFromPersons == 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 approveReceive = async (code) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await axios.post(`/api/approval/approve/receive/${code}`);
|
||||
|
||||
await loadData();
|
||||
|
||||
if (response.data.success) {
|
||||
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: 'قبول' });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const unapproveReceive = async (code) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await axios.post(`/api/approval/unapprove/receive/${code}`);
|
||||
|
||||
await loadData();
|
||||
|
||||
if (response.data.success) {
|
||||
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: 'قبول' });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const approveSelectedReceives = async () => {
|
||||
if (selectedItems.value.length === 0) {
|
||||
Swal.fire({ text: 'هیچ موردی انتخاب نشده است.', icon: 'warning', confirmButtonText: 'قبول' });
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedReceives = items.value.filter(rec => selectedItems.value.some(sel => sel.code === rec.code));
|
||||
if (selectedReceives.some(rec => !(!rec.isApproved && rec.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 {
|
||||
await axios.post(`/api/approval/approve/group/receive`, {
|
||||
'docIds': selectedItems.value.map(item => item.code)
|
||||
});
|
||||
Swal.fire({ text: 'اسناد تایید شدند.', icon: 'success', confirmButtonText: 'قبول' });
|
||||
selectedItems.value = [];
|
||||
await loadData();
|
||||
} catch (e) {
|
||||
Swal.fire({ text: 'خطا در تایید اسناد', 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: { receiveFromPersons: null } };
|
||||
} catch (error) {
|
||||
console.error('Error loading business info:', error);
|
||||
business.value = { requireTwoStepApproval: false, approvers: { receiveFromPersons: 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(() => {
|
||||
loadColumnSettings();
|
||||
loadBusinessInfo();
|
||||
loadCurrentUser();
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue