From b94ae7733e38eb377188c8862b7541567eb01bbe Mon Sep 17 00:00:00 2001 From: Babak Alizadeh Date: Fri, 22 Aug 2025 16:18:44 +0000 Subject: [PATCH] bug fix in approval system and syart working on hrm plugin --- .../migrations/Version20250822072930.php | 47 ++ .../src/Controller/BusinessController.php | 3 + hesabixCore/src/Controller/CostController.php | 2 +- .../src/Controller/DirectHesabdariDoc.php | 2 +- .../src/Controller/HesabdariController.php | 2 +- .../src/Controller/IncomeController.php | 2 +- .../src/Controller/PluginController.php | 9 + .../Plugins/Hrm/AttendanceController.php | 705 ++++++++++++++++++ hesabixCore/src/Entity/Permission.php | 15 + hesabixCore/src/Entity/PlugHrmAttendance.php | 177 +++++ .../src/Entity/PlugHrmAttendanceItem.php | 99 +++ .../PlugHrmAttendanceItemRepository.php | 54 ++ .../PlugHrmAttendanceRepository.php | 98 +++ webUI/src/router/index.ts | 40 + webUI/src/views/acc/App.vue | 13 + .../views/acc/plugins/hrm/attendance/list.vue | 333 +++++++++ .../views/acc/plugins/hrm/attendance/mod.vue | 367 +++++++++ .../acc/plugins/hrm/attendance/reports.vue | 314 ++++++++ .../views/acc/plugins/hrm/attendance/view.vue | 195 +++++ .../src/views/acc/settings/user_perm_edit.vue | 13 + 20 files changed, 2486 insertions(+), 4 deletions(-) create mode 100644 hesabixCore/migrations/Version20250822072930.php create mode 100644 hesabixCore/src/Controller/Plugins/Hrm/AttendanceController.php create mode 100644 hesabixCore/src/Entity/PlugHrmAttendance.php create mode 100644 hesabixCore/src/Entity/PlugHrmAttendanceItem.php create mode 100644 hesabixCore/src/Repository/PlugHrmAttendanceItemRepository.php create mode 100644 hesabixCore/src/Repository/PlugHrmAttendanceRepository.php create mode 100644 webUI/src/views/acc/plugins/hrm/attendance/list.vue create mode 100644 webUI/src/views/acc/plugins/hrm/attendance/mod.vue create mode 100644 webUI/src/views/acc/plugins/hrm/attendance/reports.vue create mode 100644 webUI/src/views/acc/plugins/hrm/attendance/view.vue diff --git a/hesabixCore/migrations/Version20250822072930.php b/hesabixCore/migrations/Version20250822072930.php new file mode 100644 index 0000000..b9f30c4 --- /dev/null +++ b/hesabixCore/migrations/Version20250822072930.php @@ -0,0 +1,47 @@ +addSql(<<<'SQL' + ALTER TABLE permission DROP plugHrmAttendance + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE plug_hrm_attendance CHANGE total_hours total_hours INT DEFAULT NULL, CHANGE overtime_hours overtime_hours INT DEFAULT NULL, CHANGE created_at created_at DATETIME NOT NULL, CHANGE updated_at updated_at DATETIME NOT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE plug_hrm_attendance_item CHANGE created_at created_at DATETIME NOT NULL + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE permission ADD plugHrmAttendance TINYINT(1) DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE plug_hrm_attendance CHANGE total_hours total_hours INT NOT NULL, CHANGE overtime_hours overtime_hours INT NOT NULL, CHANGE created_at created_at INT NOT NULL, CHANGE updated_at updated_at INT NOT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE plug_hrm_attendance_item CHANGE created_at created_at INT NOT NULL + SQL); + } +} diff --git a/hesabixCore/src/Controller/BusinessController.php b/hesabixCore/src/Controller/BusinessController.php index 18d7fbb..53d32ab 100644 --- a/hesabixCore/src/Controller/BusinessController.php +++ b/hesabixCore/src/Controller/BusinessController.php @@ -587,6 +587,7 @@ class BusinessController extends AbstractController 'ai' => true, 'warehouseManager' => true, 'importWorkflow' => true, + 'plugHrmAttendance' => true, ]; } elseif ($perm) { $result = [ @@ -636,6 +637,7 @@ class BusinessController extends AbstractController 'ai' => $perm->isAi(), 'warehouseManager' => $perm->isWarehouseManager(), 'importWorkflow' => $perm->isImportWorkflow(), + 'plugHrmAttendance' => $perm->isPlugHrmAttendance(), ]; if ($perm->isWarehouseManager()) { @@ -718,6 +720,7 @@ class BusinessController extends AbstractController $perm->setAi($params['ai']); $perm->setWarehouseManager($params['warehouseManager'] ?? false); $perm->setImportWorkflow($params['importWorkflow'] ?? false); + $perm->setPlugHrmAttendance($params['plugHrmAttendance'] ?? false); $entityManager->persist($perm); $entityManager->flush(); $log->insert('تنظیمات پایه', 'ویرایش دسترسی‌های کاربر با پست الکترونیکی ' . $user->getEmail(), $this->getUser(), $business); diff --git a/hesabixCore/src/Controller/CostController.php b/hesabixCore/src/Controller/CostController.php index b7bcc97..1d7a04e 100644 --- a/hesabixCore/src/Controller/CostController.php +++ b/hesabixCore/src/Controller/CostController.php @@ -621,7 +621,7 @@ class CostController extends AbstractController // Set approval status based on business settings $business = $acc['bid']; - if ($business->getTwoStepApproval()) { + if ($business->isRequireTwoStepApproval()) { // Two-step approval is enabled $doc->setIsPreview(true); $doc->setIsApproved(false); diff --git a/hesabixCore/src/Controller/DirectHesabdariDoc.php b/hesabixCore/src/Controller/DirectHesabdariDoc.php index b403c8e..d9e7fbc 100644 --- a/hesabixCore/src/Controller/DirectHesabdariDoc.php +++ b/hesabixCore/src/Controller/DirectHesabdariDoc.php @@ -44,7 +44,7 @@ class DirectHesabdariDoc extends AbstractController // Set approval status based on business settings $business = $acc['bid']; - if ($business->getTwoStepApproval()) { + if ($business->isRequireTwoStepApproval()) { // Two-step approval is enabled $hesabdariDoc->setIsPreview(true); $hesabdariDoc->setIsApproved(false); diff --git a/hesabixCore/src/Controller/HesabdariController.php b/hesabixCore/src/Controller/HesabdariController.php index 8dd50bd..f3b51ae 100644 --- a/hesabixCore/src/Controller/HesabdariController.php +++ b/hesabixCore/src/Controller/HesabdariController.php @@ -477,7 +477,7 @@ class HesabdariController extends AbstractController // Set approval status based on business settings $business = $acc['bid']; - if ($business->getTwoStepApproval()) { + if ($business->isRequireTwoStepApproval()) { // Two-step approval is enabled $doc->setIsPreview(true); $doc->setIsApproved(false); diff --git a/hesabixCore/src/Controller/IncomeController.php b/hesabixCore/src/Controller/IncomeController.php index a6b202b..8ac50df 100644 --- a/hesabixCore/src/Controller/IncomeController.php +++ b/hesabixCore/src/Controller/IncomeController.php @@ -618,7 +618,7 @@ class IncomeController extends AbstractController // Set approval status based on business settings $business = $acc['bid']; - if ($business->getTwoStepApproval()) { + if ($business->isRequireTwoStepApproval()) { // Two-step approval is enabled $doc->setIsPreview(true); $doc->setIsApproved(false); diff --git a/hesabixCore/src/Controller/PluginController.php b/hesabixCore/src/Controller/PluginController.php index 44127b1..53307d4 100644 --- a/hesabixCore/src/Controller/PluginController.php +++ b/hesabixCore/src/Controller/PluginController.php @@ -515,6 +515,15 @@ class PluginController extends AbstractController 'icon' => 'import-workflow.png', 'defaultOn' => null, ], + [ + 'name' => ' مدیریت منابع انسانی', + 'code' => 'hrm', + 'timestamp' => '32104000', + 'timelabel' => 'یک سال', + 'price' => '200000', + 'icon' => 'hmr.jpg', + 'defaultOn' => null, + ], ]; $repo = $entityManager->getRepository(PluginProdect::class); diff --git a/hesabixCore/src/Controller/Plugins/Hrm/AttendanceController.php b/hesabixCore/src/Controller/Plugins/Hrm/AttendanceController.php new file mode 100644 index 0000000..c3b74bc --- /dev/null +++ b/hesabixCore/src/Controller/Plugins/Hrm/AttendanceController.php @@ -0,0 +1,705 @@ +entityManager = $entityManager; + } + + #[Route('/api/hrm/attendance/list', name: 'hrm_attendance_list', methods: ['POST'])] + public function list(Request $request, Access $access, Jdate $jdate): JsonResponse + { + try { + $params = []; + if ($content = $request->getContent()) { + $params = json_decode($content, true); + } + + $acc = $access->hasRole('plugHrmAttendance'); + if (!$acc) { + throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.'); + } + + // دریافت پارامترهای فیلتر + $page = $params['page'] ?? 1; + $limit = $params['limit'] ?? 20; + $fromDate = $params['fromDate'] ?? null; + $toDate = $params['toDate'] ?? null; + $personId = $params['personId'] ?? null; + + // ایجاد کوئری + $qb = $this->entityManager->createQueryBuilder(); + $qb->select('a') + ->from(PlugHrmAttendance::class, 'a') + ->leftJoin('a.person', 'p') + ->addSelect('p') + ->where('a.business = :bid') + ->setParameter('bid', $acc['bid']); + + // اعمال فیلترها + if ($fromDate) { + $qb->andWhere('a.date >= :fromDate') + ->setParameter('fromDate', $fromDate); + } + + if ($toDate) { + $qb->andWhere('a.date <= :toDate') + ->setParameter('toDate', $toDate); + } + + if ($personId) { + $qb->andWhere('a.person = :personId') + ->setParameter('personId', $personId); + } + + $qb->orderBy('a.date', 'DESC') + ->addOrderBy('p.nikename', 'ASC'); + + // محاسبه تعداد کل + $countQb = clone $qb; + $totalCount = $countQb->select('COUNT(a.id)')->getQuery()->getSingleScalarResult(); + + // اعمال صفحه‌بندی + $qb->setFirstResult(($page - 1) * $limit) + ->setMaxResults($limit); + + $attendances = $qb->getQuery()->getResult(); + + // تبدیل به آرایه + $result = []; + foreach ($attendances as $attendance) { + $result[] = [ + 'id' => $attendance->getId(), + 'date' => $attendance->getDate(), + 'personId' => $attendance->getPerson()->getId(), + 'personName' => $attendance->getPerson()->getNikename(), + 'totalHours' => $attendance->getTotalHours(), + 'overtimeHours' => $attendance->getOvertimeHours(), + 'description' => $attendance->getDescription(), + 'createdAt' => $jdate->jdate('Y/n/d H:i', $attendance->getCreatedAt()->getTimestamp()), + ]; + } + + return $this->json([ + 'data' => $result, + 'total' => $totalCount, + 'page' => $page, + 'limit' => $limit, + 'pages' => ceil($totalCount / $limit) + ]); + + } catch (\Exception $e) { + return $this->json(['error' => $e->getMessage()], 500); + } + } + + #[Route('/api/hrm/attendance/mod/{id}', name: 'hrm_attendance_mod', methods: ['POST'])] + public function mod(Request $request, Access $access, Log $log, Jdate $jdate, $id = 0): JsonResponse + { + try { + $params = []; + if ($content = $request->getContent()) { + $params = json_decode($content, true); + } + + $acc = $access->hasRole('plugHrmAttendance'); + if (!$acc) { + throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.'); + } + + if ($id == 0) { + // ایجاد تردد جدید + $attendance = new PlugHrmAttendance(); + $attendance->setBusiness($acc['bid']); + } else { + // ویرایش تردد موجود + $attendance = $this->entityManager->getRepository(PlugHrmAttendance::class)->find($id); + if (!$attendance || $attendance->getBusiness()->getId() != $acc['bid']->getId()) { + throw $this->createNotFoundException('تردد یافت نشد.'); + } + } + + // بررسی وجود پرسنل + $person = $this->entityManager->getRepository(Person::class)->find($params['personId']); + if (!$person || $person->getBid()->getId() != $acc['bid']->getId()) { + throw $this->createNotFoundException('پرسنل یافت نشد.'); + } + + $attendance->setPerson($person); + $attendance->setDate($params['date']); + $attendance->setDescription($params['description'] ?? ''); + $attendance->setUpdatedAt(new \DateTime()); + + // محاسبه ساعات کار + $totalHours = 0; + $overtimeHours = 0; + if (isset($params['items']) && is_array($params['items'])) { + // حذف آیتم‌های قبلی + foreach ($attendance->getItems() as $item) { + $this->entityManager->remove($item); + } + + // افزودن آیتم‌های جدید + foreach ($params['items'] as $itemData) { + $item = new PlugHrmAttendanceItem(); + $item->setAttendance($attendance); + $item->setType($itemData['type']); // ورود یا خروج + $item->setTime($itemData['time']); // HH:MM + $item->setTimestamp($itemData['timestamp']); + $this->entityManager->persist($item); + } + + // محاسبه ساعات کار + $workHours = $this->calculateWorkHours($params['items']); + $totalHours = $workHours['total']; + $overtimeHours = $workHours['overtime']; + } + + $attendance->setTotalHours($totalHours); + $attendance->setOvertimeHours($overtimeHours); + + $this->entityManager->persist($attendance); + $this->entityManager->flush(); + + $log->insert( + 'تردد پرسنل', + 'تردد پرسنل ' . $person->getNikename() . ' برای تاریخ ' . $params['date'] . ' ثبت شد.', + $this->getUser(), + $acc['bid'] + ); + + return $this->json(['result' => 1, 'id' => $attendance->getId()]); + + } catch (\Exception $e) { + return $this->json(['error' => $e->getMessage()], 500); + } + } + + #[Route('/api/hrm/attendance/get/{id}', name: 'hrm_attendance_get', methods: ['POST'])] + public function get(Request $request, Access $access, Jdate $jdate, $id): JsonResponse + { + try { + $acc = $access->hasRole('plugHrmAttendance'); + if (!$acc) { + throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.'); + } + + $attendance = $this->entityManager->getRepository(PlugHrmAttendance::class)->find($id); + if (!$attendance || $attendance->getBusiness()->getId() != $acc['bid']->getId()) { + throw $this->createNotFoundException('تردد یافت نشد.'); + } + + $items = []; + foreach ($attendance->getItems() as $item) { + $items[] = [ + 'id' => $item->getId(), + 'type' => $item->getType(), + 'time' => $item->getTime(), + 'timestamp' => $item->getTimestamp(), + ]; + } + + $result = [ + 'id' => $attendance->getId(), + 'personId' => $attendance->getPerson()->getId(), + 'personName' => $attendance->getPerson()->getNikename(), + 'date' => $attendance->getDate(), + 'totalHours' => $attendance->getTotalHours(), + 'overtimeHours' => $attendance->getOvertimeHours(), + 'description' => $attendance->getDescription(), + 'items' => $items, + ]; + + return $this->json($result); + + } catch (\Exception $e) { + return $this->json(['error' => $e->getMessage()], 500); + } + } + + #[Route('/api/hrm/attendance/delete/{id}', name: 'hrm_attendance_delete', methods: ['POST'])] + public function delete(Request $request, Access $access, Log $log, $id): JsonResponse + { + try { + $acc = $access->hasRole('plugHrmAttendance'); + if (!$acc) { + throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.'); + } + + $attendance = $this->entityManager->getRepository(PlugHrmAttendance::class)->find($id); + if (!$attendance || $attendance->getBusiness()->getId() != $acc['bid']->getId()) { + throw $this->createNotFoundException('تردد یافت نشد.'); + } + + $personName = $attendance->getPerson()->getNikename(); + $date = $attendance->getDate(); + + $this->entityManager->remove($attendance); + $this->entityManager->flush(); + + $log->insert( + 'تردد پرسنل', + 'تردد پرسنل ' . $personName . ' برای تاریخ ' . $date . ' حذف شد.', + $this->getUser(), + $acc['bid'] + ); + + return $this->json(['result' => 1]); + + } catch (\Exception $e) { + return $this->json(['error' => $e->getMessage()], 500); + } + } + + #[Route('/api/hrm/attendance/employees', name: 'hrm_attendance_employees', methods: ['POST'])] + public function getEmployees(Request $request, Access $access): JsonResponse + { + try { + $acc = $access->hasRole('plugHrmAttendance'); + if (!$acc) { + throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.'); + } + + // دریافت نوع پرسنل "کارمند" یا "employee" + $employeeType = $this->entityManager->getRepository(PersonType::class)->findOneBy(['code' => 'employee']); + if (!$employeeType) { + // اگر نوع "employee" وجود نداشت، نوع "کارمند" را جستجو کن + $employeeType = $this->entityManager->getRepository(PersonType::class)->findOneBy(['code' => 'کارمند']); + } + + $qb = $this->entityManager->createQueryBuilder(); + $qb->select('p') + ->from(Person::class, 'p') + ->where('p.bid = :bid') + ->setParameter('bid', $acc['bid']); + + if ($employeeType) { + $qb->join('p.type', 't') + ->andWhere('t = :employeeType') + ->setParameter('employeeType', $employeeType); + } + + $qb->orderBy('p.nikename', 'ASC'); + + $employees = $qb->getQuery()->getResult(); + + $result = []; + foreach ($employees as $employee) { + $result[] = [ + 'id' => $employee->getId(), + 'name' => $employee->getNikename(), + 'code' => $employee->getCode(), + ]; + } + + return $this->json($result); + + } catch (\Exception $e) { + return $this->json(['error' => $e->getMessage()], 500); + } + } + + #[Route('/api/hrm/attendance/import', name: 'hrm_attendance_import', methods: ['POST'])] + public function import(Request $request, Access $access, Log $log, Jdate $jdate): JsonResponse + { + try { + $acc = $access->hasRole('plugHrmAttendance'); + if (!$acc) { + throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.'); + } + + $file = $request->files->get('file'); + if (!$file) { + throw new \Exception('فایل انتخاب نشده است.'); + } + + $spreadsheet = IOFactory::load($file->getPathname()); + $worksheet = $spreadsheet->getActiveSheet(); + $rows = $worksheet->toArray(); + + // حذف سطر هدر + array_shift($rows); + + $importedCount = 0; + $errors = []; + + foreach ($rows as $index => $row) { + try { + if (empty($row[0]) || empty($row[1]) || empty($row[2])) { + continue; // رد کردن سطرهای خالی + } + + $personCode = $row[0]; + $date = $row[1]; + $time = $row[2]; + $type = $row[3] ?? 'ورود'; // ورود یا خروج + + // پیدا کردن پرسنل بر اساس کد + $person = $this->entityManager->getRepository(Person::class)->findOneBy([ + 'code' => $personCode, + 'bid' => $acc['bid'] + ]); + + if (!$person) { + $errors[] = "سطر " . ($index + 2) . ": پرسنل با کد $personCode یافت نشد."; + continue; + } + + // بررسی وجود تردد برای این تاریخ + $attendance = $this->entityManager->getRepository(PlugHrmAttendance::class) + ->findByBusinessAndPersonAndDate($acc['bid'], $person, $date); + + if (!$attendance) { + $attendance = new PlugHrmAttendance(); + $attendance->setBusiness($acc['bid']); + $attendance->setPerson($person); + $attendance->setDate($date); + $attendance->setTotalHours(0); + $attendance->setOvertimeHours(0); + } + + // افزودن آیتم تردد + $item = new PlugHrmAttendanceItem(); + $item->setAttendance($attendance); + $item->setType($type); + $item->setTime($time); + $item->setTimestamp($jdate->jallaliToUnixTime($date . ' ' . $time)); + + $this->entityManager->persist($item); + $this->entityManager->persist($attendance); + $importedCount++; + + } catch (\Exception $e) { + $errors[] = "سطر " . ($index + 2) . ": " . $e->getMessage(); + } + } + + $this->entityManager->flush(); + + $log->insert( + 'تردد پرسنل', + "$importedCount رکورد تردد از فایل اکسل وارد شد.", + $this->getUser(), + $acc['bid'] + ); + + return $this->json([ + 'result' => 1, + 'importedCount' => $importedCount, + 'errors' => $errors + ]); + + } catch (\Exception $e) { + return $this->json(['error' => $e->getMessage()], 500); + } + } + + #[Route('/api/hrm/attendance/export', name: 'hrm_attendance_export', methods: ['POST'])] + public function export(Request $request, Access $access, Jdate $jdate): JsonResponse + { + try { + $params = []; + if ($content = $request->getContent()) { + $params = json_decode($content, true); + } + + $acc = $access->hasRole('plugHrmAttendance'); + if (!$acc) { + throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.'); + } + + $fromDate = $params['fromDate'] ?? null; + $toDate = $params['toDate'] ?? null; + $personId = $params['personId'] ?? null; + + $attendances = $this->entityManager->getRepository(PlugHrmAttendance::class) + ->findByBusinessAndDateRange($acc['bid'], $fromDate, $toDate, $personId); + + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + // تنظیم هدر + $sheet->setCellValue('A1', 'کد پرسنل'); + $sheet->setCellValue('B1', 'نام پرسنل'); + $sheet->setCellValue('C1', 'تاریخ'); + $sheet->setCellValue('D1', 'ساعات کل کار'); + $sheet->setCellValue('E1', 'ساعات اضافه‌کاری'); + $sheet->setCellValue('F1', 'توضیحات'); + + $row = 2; + foreach ($attendances as $attendance) { + $sheet->setCellValue('A' . $row, $attendance->getPerson()->getCode()); + $sheet->setCellValue('B' . $row, $attendance->getPerson()->getNikename()); + $sheet->setCellValue('C' . $row, $attendance->getDate()); + $sheet->setCellValue('D' . $row, $this->formatMinutesToHours($attendance->getTotalHours())); + $sheet->setCellValue('E' . $row, $this->formatMinutesToHours($attendance->getOvertimeHours())); + $sheet->setCellValue('F' . $row, $attendance->getDescription()); + $row++; + } + + $writer = new Xlsx($spreadsheet); + $filename = 'attendance_export_' . date('Y-m-d_H-i-s') . '.xlsx'; + $filepath = sys_get_temp_dir() . '/' . $filename; + $writer->save($filepath); + + return $this->json([ + 'result' => 1, + 'filename' => $filename, + 'downloadUrl' => '/api/hrm/attendance/download/' . $filename + ]); + + } catch (\Exception $e) { + return $this->json(['error' => $e->getMessage()], 500); + } + } + + #[Route('/api/hrm/attendance/download/{filename}', name: 'hrm_attendance_download', methods: ['GET'])] + public function download(Request $request, Access $access, $filename): JsonResponse + { + try { + $acc = $access->hasRole('plugHrmAttendance'); + if (!$acc) { + throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.'); + } + + $filepath = sys_get_temp_dir() . '/' . $filename; + if (!file_exists($filepath)) { + throw $this->createNotFoundException('فایل یافت نشد.'); + } + + $response = new JsonResponse(); + $response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + $response->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"'); + $response->setContent(file_get_contents($filepath)); + + unlink($filepath); // حذف فایل موقت + + return $response; + + } catch (\Exception $e) { + return $this->json(['error' => $e->getMessage()], 500); + } + } + + #[Route('/api/hrm/attendance/reports', name: 'hrm_attendance_reports', methods: ['POST'])] + public function reports(Request $request, Access $access, Jdate $jdate): JsonResponse + { + try { + $params = []; + if ($content = $request->getContent()) { + $params = json_decode($content, true); + } + + $acc = $access->hasRole('plugHrmAttendance'); + if (!$acc) { + throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.'); + } + + $type = $params['type'] ?? 'daily'; + $fromDate = $params['fromDate'] ?? null; + $toDate = $params['toDate'] ?? null; + $personId = $params['personId'] ?? null; + + $attendances = $this->entityManager->getRepository(PlugHrmAttendance::class) + ->findByBusinessAndDateRange($acc['bid'], $fromDate, $toDate, $personId); + + $reportData = []; + $summary = [ + 'totalDays' => 0, + 'totalHours' => 0, + 'totalOvertime' => 0, + 'averageHours' => 0 + ]; + + foreach ($attendances as $attendance) { + $reportData[] = [ + 'id' => $attendance->getId(), + 'date' => $attendance->getDate(), + 'personName' => $attendance->getPerson()->getNikename(), + 'personCode' => $attendance->getPerson()->getCode(), + 'totalHours' => $attendance->getTotalHours(), + 'overtimeHours' => $attendance->getOvertimeHours(), + 'description' => $attendance->getDescription(), + 'status' => $this->getAttendanceStatus($attendance) + ]; + + $summary['totalDays']++; + $summary['totalHours'] += $attendance->getTotalHours(); + $summary['totalOvertime'] += $attendance->getOvertimeHours(); + } + + if ($summary['totalDays'] > 0) { + $summary['averageHours'] = round($summary['totalHours'] / $summary['totalDays']); + } + + return $this->json([ + 'result' => 1, + 'data' => $reportData, + 'summary' => $summary + ]); + + } catch (\Exception $e) { + return $this->json(['error' => $e->getMessage()], 500); + } + } + + #[Route('/api/hrm/attendance/export-report', name: 'hrm_attendance_export_report', methods: ['POST'])] + public function exportReport(Request $request, Access $access, Jdate $jdate): JsonResponse + { + try { + $params = []; + if ($content = $request->getContent()) { + $params = json_decode($content, true); + } + + $acc = $access->hasRole('plugHrmAttendance'); + if (!$acc) { + throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.'); + } + + $type = $params['type'] ?? 'daily'; + $fromDate = $params['fromDate'] ?? null; + $toDate = $params['toDate'] ?? null; + $personId = $params['personId'] ?? null; + + $attendances = $this->entityManager->getRepository(PlugHrmAttendance::class) + ->findByBusinessAndDateRange($acc['bid'], $fromDate, $toDate, $personId); + + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + // تنظیم هدر بر اساس نوع گزارش + $headers = ['تاریخ', 'نام پرسنل', 'کد پرسنل', 'ساعات کل کار', 'ساعات اضافه‌کاری']; + if ($type === 'absence') { + $headers[] = 'وضعیت'; + } + $headers[] = 'توضیحات'; + + foreach ($headers as $index => $header) { + $sheet->setCellValue(chr(65 + $index) . '1', $header); + } + + $row = 2; + foreach ($attendances as $attendance) { + $sheet->setCellValue('A' . $row, $attendance->getDate()); + $sheet->setCellValue('B' . $row, $attendance->getPerson()->getNikename()); + $sheet->setCellValue('C' . $row, $attendance->getPerson()->getCode()); + $sheet->setCellValue('D' . $row, $this->formatMinutesToHours($attendance->getTotalHours())); + $sheet->setCellValue('E' . $row, $this->formatMinutesToHours($attendance->getOvertimeHours())); + + if ($type === 'absence') { + $sheet->setCellValue('F' . $row, $this->getAttendanceStatus($attendance)); + $sheet->setCellValue('G' . $row, $attendance->getDescription()); + } else { + $sheet->setCellValue('F' . $row, $attendance->getDescription()); + } + $row++; + } + + $writer = new Xlsx($spreadsheet); + $filename = 'attendance_report_' . $type . '_' . date('Y-m-d_H-i-s') . '.xlsx'; + $filepath = sys_get_temp_dir() . '/' . $filename; + $writer->save($filepath); + + return $this->json([ + 'result' => 1, + 'filename' => $filename, + 'downloadUrl' => '/api/hrm/attendance/download/' . $filename + ]); + + } catch (\Exception $e) { + return $this->json(['error' => $e->getMessage()], 500); + } + } + + private function getAttendanceStatus(PlugHrmAttendance $attendance): string + { + $totalHours = $attendance->getTotalHours(); + $overtimeHours = $attendance->getOvertimeHours(); + + // استاندارد 8 ساعت کار در روز + $standardHours = 8 * 60; // به دقیقه + + if ($totalHours >= $standardHours) { + return 'حضور کامل'; + } elseif ($totalHours >= $standardHours * 0.5) { + return 'نیمه وقت'; + } elseif ($totalHours > 0) { + return 'تاخیر'; + } else { + return 'غیبت'; + } + } + + private function calculateWorkHours($items): array + { + $totalMinutes = 0; + $overtimeMinutes = 0; + $standardWorkMinutes = 8 * 60; // 8 ساعت کار استاندارد + + // مرتب کردن آیتم‌ها بر اساس زمان + usort($items, function($a, $b) { + return strtotime($a['time']) - strtotime($b['time']); + }); + + $entries = []; + $exits = []; + + foreach ($items as $item) { + if ($item['type'] === 'ورود') { + $entries[] = $item['time']; + } elseif ($item['type'] === 'خروج') { + $exits[] = $item['time']; + } + } + + // محاسبه ساعات کار + $minCount = min(count($entries), count($exits)); + for ($i = 0; $i < $minCount; $i++) { + $entryTime = strtotime($entries[$i]); + $exitTime = strtotime($exits[$i]); + $workMinutes = ($exitTime - $entryTime) / 60; + $totalMinutes += $workMinutes; + } + + if ($totalMinutes > $standardWorkMinutes) { + $overtimeMinutes = $totalMinutes - $standardWorkMinutes; + $totalMinutes = $standardWorkMinutes; + } + + return [ + 'total' => $totalMinutes, + 'overtime' => $overtimeMinutes + ]; + } + + private function formatMinutesToHours($minutes): string + { + if (!$minutes) return '0:00'; + + $hours = floor($minutes / 60); + $mins = $minutes % 60; + return sprintf('%d:%02d', $hours, $mins); + } +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/Permission.php b/hesabixCore/src/Entity/Permission.php index 48ac1a0..bd74f00 100644 --- a/hesabixCore/src/Entity/Permission.php +++ b/hesabixCore/src/Entity/Permission.php @@ -147,6 +147,9 @@ class Permission #[ORM\Column(nullable: true)] private ?bool $importWorkflow = null; + #[ORM\Column(nullable: true)] + private ?bool $plugHrmAttendance = null; + public function getId(): ?int { return $this->id; @@ -679,4 +682,16 @@ class Permission return $this; } + + public function isPlugHrmAttendance(): ?bool + { + return $this->plugHrmAttendance; + } + + public function setPlugHrmAttendance(?bool $plugHrmAttendance): static + { + $this->plugHrmAttendance = $plugHrmAttendance; + + return $this; + } } \ No newline at end of file diff --git a/hesabixCore/src/Entity/PlugHrmAttendance.php b/hesabixCore/src/Entity/PlugHrmAttendance.php new file mode 100644 index 0000000..6317b06 --- /dev/null +++ b/hesabixCore/src/Entity/PlugHrmAttendance.php @@ -0,0 +1,177 @@ +items = new ArrayCollection(); + $this->createdAt = new \DateTime(); + $this->updatedAt = new \DateTime(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getBusiness(): ?Business + { + return $this->business; + } + + public function setBusiness(?Business $business): static + { + $this->business = $business; + return $this; + } + + public function getPerson(): ?Person + { + return $this->person; + } + + public function setPerson(?Person $person): static + { + $this->person = $person; + return $this; + } + + public function getDate(): ?string + { + return $this->date; + } + + public function setDate(string $date): static + { + $this->date = $date; + return $this; + } + + public function getTotalHours(): ?int + { + return $this->totalHours; + } + + public function setTotalHours(?int $totalHours): static + { + $this->totalHours = $totalHours; + return $this; + } + + public function getOvertimeHours(): ?int + { + return $this->overtimeHours; + } + + public function setOvertimeHours(?int $overtimeHours): static + { + $this->overtimeHours = $overtimeHours; + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): static + { + $this->description = $description; + return $this; + } + + public function getCreatedAt(): ?\DateTimeInterface + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeInterface $createdAt): static + { + $this->createdAt = $createdAt; + return $this; + } + + public function getUpdatedAt(): ?\DateTimeInterface + { + return $this->updatedAt; + } + + public function setUpdatedAt(\DateTimeInterface $updatedAt): static + { + $this->updatedAt = $updatedAt; + return $this; + } + + /** + * @return Collection + */ + public function getItems(): Collection + { + return $this->items; + } + + public function addItem(PlugHrmAttendanceItem $item): static + { + if (!$this->items->contains($item)) { + $this->items->add($item); + $item->setAttendance($this); + } + return $this; + } + + public function removeItem(PlugHrmAttendanceItem $item): static + { + if ($this->items->removeElement($item)) { + if ($item->getAttendance() === $this) { + $item->setAttendance(null); + } + } + return $this; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/PlugHrmAttendanceItem.php b/hesabixCore/src/Entity/PlugHrmAttendanceItem.php new file mode 100644 index 0000000..9138151 --- /dev/null +++ b/hesabixCore/src/Entity/PlugHrmAttendanceItem.php @@ -0,0 +1,99 @@ +createdAt = new \DateTime(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getAttendance(): ?PlugHrmAttendance + { + return $this->attendance; + } + + public function setAttendance(?PlugHrmAttendance $attendance): static + { + $this->attendance = $attendance; + return $this; + } + + public function getType(): ?string + { + return $this->type; + } + + public function setType(string $type): static + { + $this->type = $type; + return $this; + } + + public function getTime(): ?string + { + return $this->time; + } + + public function setTime(string $time): static + { + $this->time = $time; + return $this; + } + + public function getTimestamp(): ?int + { + return $this->timestamp; + } + + public function setTimestamp(int $timestamp): static + { + $this->timestamp = $timestamp; + return $this; + } + + public function getCreatedAt(): ?\DateTimeInterface + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeInterface $createdAt): static + { + $this->createdAt = $createdAt; + return $this; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Repository/PlugHrmAttendanceItemRepository.php b/hesabixCore/src/Repository/PlugHrmAttendanceItemRepository.php new file mode 100644 index 0000000..d6a9b8a --- /dev/null +++ b/hesabixCore/src/Repository/PlugHrmAttendanceItemRepository.php @@ -0,0 +1,54 @@ + + * + * @method PlugHrmAttendanceItem|null find($id, $lockMode = null, $lockVersion = null) + * @method PlugHrmAttendanceItem|null findOneBy(array $criteria, array $orderBy = null) + * @method PlugHrmAttendanceItem[] findAll() + * @method PlugHrmAttendanceItem[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class PlugHrmAttendanceItemRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, PlugHrmAttendanceItem::class); + } + + public function save(PlugHrmAttendanceItem $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(PlugHrmAttendanceItem $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * @return PlugHrmAttendanceItem[] Returns an array of PlugHrmAttendanceItem objects + */ + public function findByAttendanceOrderedByTime($attendance): array + { + return $this->createQueryBuilder('i') + ->where('i.attendance = :attendance') + ->setParameter('attendance', $attendance) + ->orderBy('i.time', 'ASC') + ->getQuery() + ->getResult(); + } +} \ No newline at end of file diff --git a/hesabixCore/src/Repository/PlugHrmAttendanceRepository.php b/hesabixCore/src/Repository/PlugHrmAttendanceRepository.php new file mode 100644 index 0000000..f1dbbdb --- /dev/null +++ b/hesabixCore/src/Repository/PlugHrmAttendanceRepository.php @@ -0,0 +1,98 @@ + + * + * @method PlugHrmAttendance|null find($id, $lockMode = null, $lockVersion = null) + * @method PlugHrmAttendance|null findOneBy(array $criteria, array $orderBy = null) + * @method PlugHrmAttendance[] findAll() + * @method PlugHrmAttendance[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class PlugHrmAttendanceRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, PlugHrmAttendance::class); + } + + public function save(PlugHrmAttendance $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(PlugHrmAttendance $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * @return PlugHrmAttendance[] Returns an array of PlugHrmAttendance objects + */ + public function findByBusinessAndDateRange($business, $fromDate, $toDate, $personId = null): array + { + $qb = $this->createQueryBuilder('a') + ->leftJoin('a.person', 'p') + ->addSelect('p') + ->where('a.business = :business') + ->andWhere('a.date >= :fromDate') + ->andWhere('a.date <= :toDate') + ->setParameter('business', $business) + ->setParameter('fromDate', $fromDate) + ->setParameter('toDate', $toDate) + ->orderBy('a.date', 'DESC') + ->addOrderBy('p.nikename', 'ASC'); + + if ($personId) { + $qb->andWhere('a.person = :personId') + ->setParameter('personId', $personId); + } + + return $qb->getQuery()->getResult(); + } + + /** + * @return PlugHrmAttendance[] Returns an array of PlugHrmAttendance objects + */ + public function findByBusinessAndPerson($business, $person, $limit = 30): array + { + return $this->createQueryBuilder('a') + ->where('a.business = :business') + ->andWhere('a.person = :person') + ->setParameter('business', $business) + ->setParameter('person', $person) + ->orderBy('a.date', 'DESC') + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + } + + /** + * @return PlugHrmAttendance|null Returns a single PlugHrmAttendance object + */ + public function findByBusinessAndPersonAndDate($business, $person, $date): ?PlugHrmAttendance + { + return $this->createQueryBuilder('a') + ->where('a.business = :business') + ->andWhere('a.person = :person') + ->andWhere('a.date = :date') + ->setParameter('business', $business) + ->setParameter('person', $person) + ->setParameter('date', $date) + ->getQuery() + ->getOneOrNullResult(); + } +} \ No newline at end of file diff --git a/webUI/src/router/index.ts b/webUI/src/router/index.ts index 095132d..2d02ec5 100755 --- a/webUI/src/router/index.ts +++ b/webUI/src/router/index.ts @@ -1070,6 +1070,46 @@ const router = createRouter({ component: () => import('../views/acc/plugins/hrm/docs/view.vue'), }, + { + path: 'hrm/attendance/list', + name: 'hrm_attendance_list', + component: () => + import('../views/acc/plugins/hrm/attendance/list.vue'), + meta: { + 'title': 'مدیریت تردد پرسنل', + 'login': true, + } + }, + { + path: 'hrm/attendance/mod/:id?', + name: 'hrm_attendance_mod', + component: () => + import('../views/acc/plugins/hrm/attendance/mod.vue'), + meta: { + 'title': 'افزودن/ویرایش تردد', + 'login': true, + } + }, + { + path: 'hrm/attendance/view/:id', + name: 'hrm_attendance_view', + component: () => + import('../views/acc/plugins/hrm/attendance/view.vue'), + meta: { + 'title': 'مشاهده جزئیات تردد', + 'login': true, + } + }, + { + path: 'hrm/attendance/reports', + name: 'hrm_attendance_reports', + component: () => + import('../views/acc/plugins/hrm/attendance/reports.vue'), + meta: { + 'title': 'گزارشات تردد پرسنل', + 'login': true, + } + }, { path: 'inquiry/panel', name: 'inquiry_panel', diff --git a/webUI/src/views/acc/App.vue b/webUI/src/views/acc/App.vue index 80b531e..5a9a97f 100755 --- a/webUI/src/views/acc/App.vue +++ b/webUI/src/views/acc/App.vue @@ -881,6 +881,19 @@ export default { + + + تردد پرسنل + {{ getShortcutKey('/acc/hrm/attendance/list') }} + + +