progress in two step approval

This commit is contained in:
Hesabix 2025-09-03 02:27:31 +03:30
parent 822402cfda
commit 3d454a642f
2 changed files with 311 additions and 8 deletions

View file

@ -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'])] #[Route('/api/person/receive/list/search', name: 'app_persons_receive_list_search', methods: ['POST'])]
public function app_persons_receive_list_search( public function app_persons_receive_list_search(
Request $request, Request $request,
@ -1134,6 +1222,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';
@ -1157,17 +1246,33 @@ 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, 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') ->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_receive') ->setParameter('type', 'person_receive')
->setParameter('year', $acc['year']) ->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']) ->setParameter('money', $acc['money']);
// اعمال فیلتر وضعیت تایید
if ($approvalFilter === 'approved') {
$queryBuilder->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true); ->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)) { if (!empty($search)) {
@ -1218,12 +1323,24 @@ 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_receive') ->setParameter('type', 'person_receive')
->setParameter('year', $acc['year']) ->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']) ->setParameter('money', $acc['money']);
// اعمال فیلتر وضعیت تایید برای کوئری تعداد
if ($approvalFilter === 'approved') {
$totalQueryBuilder->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true); ->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)) { if (!empty($search)) {
@ -1389,6 +1506,14 @@ 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'],
'submitter' => $doc['submitter'],
'approvedBy' => $doc['approvedByName'] ? [
'fullName' => $doc['approvedByName'],
'id' => $doc['approvedById'],
'email' => $doc['approvedByEmail']
] : null,
]; ];
} }

View file

@ -14,6 +14,12 @@
<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">
@ -69,6 +75,13 @@
</template> </template>
</v-tooltip> </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"
@ -161,6 +174,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="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 }}"> <v-list-item :to="{ name: 'person_receive_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>
@ -205,6 +230,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>
@ -285,6 +318,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: { receiveFromPersons: 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 },
@ -295,13 +332,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 = [
@ -345,6 +389,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'
@ -616,8 +661,141 @@ 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.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(() => { onMounted(() => {
loadColumnSettings(); loadColumnSettings();
loadBusinessInfo();
loadCurrentUser();
loadData(); loadData();
}); });
</script> </script>