Compare commits
2 commits
043caee783
...
1418591120
Author | SHA1 | Date | |
---|---|---|---|
|
1418591120 | ||
|
b94ae7733e |
47
hesabixCore/migrations/Version20250822072930.php
Normal file
47
hesabixCore/migrations/Version20250822072930.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250822072930 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->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);
|
||||
}
|
||||
}
|
|
@ -586,6 +586,9 @@ class BusinessController extends AbstractController
|
|||
'plugImportWorkflow' => true,
|
||||
'inquiry' => true,
|
||||
'ai' => true,
|
||||
'warehouseManager' => true,
|
||||
'importWorkflow' => true,
|
||||
'plugHrmAttendance' => true,
|
||||
'storehelper' => true,
|
||||
];
|
||||
} elseif ($perm) {
|
||||
|
@ -635,6 +638,9 @@ class BusinessController extends AbstractController
|
|||
'plugImportWorkflow' => $perm->isImportWorkflow(),
|
||||
'inquiry' => $perm->isInquiry(),
|
||||
'ai' => $perm->isAi(),
|
||||
'warehouseManager' => $perm->isWarehouseManager(),
|
||||
'importWorkflow' => $perm->isImportWorkflow(),
|
||||
'plugHrmAttendance' => $perm->isPlugHrmAttendance(),
|
||||
'storehelper' => $perm->isStorehelper()
|
||||
];
|
||||
}
|
||||
|
@ -710,6 +716,9 @@ class BusinessController extends AbstractController
|
|||
$perm->setImportWorkflow($params['plugImportWorkflow'] ?? false);
|
||||
$perm->setInquiry($params['inquiry']);
|
||||
$perm->setAi($params['ai']);
|
||||
$perm->setWarehouseManager($params['warehouseManager'] ?? false);
|
||||
$perm->setImportWorkflow($params['importWorkflow'] ?? false);
|
||||
$perm->setPlugHrmAttendance($params['plugHrmAttendance'] ?? false);
|
||||
$perm->setStorehelper($params['storehelper'] ?? false);
|
||||
$entityManager->persist($perm);
|
||||
$entityManager->flush();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
705
hesabixCore/src/Controller/Plugins/Hrm/AttendanceController.php
Normal file
705
hesabixCore/src/Controller/Plugins/Hrm/AttendanceController.php
Normal file
|
@ -0,0 +1,705 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller\Plugins\Hrm;
|
||||
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use App\Entity\PlugHrmAttendance;
|
||||
use App\Entity\PlugHrmAttendanceItem;
|
||||
use App\Entity\Person;
|
||||
use App\Entity\PersonType;
|
||||
use App\Service\Access;
|
||||
use App\Service\Log;
|
||||
use App\Service\Jdate;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
|
||||
class AttendanceController extends AbstractController
|
||||
{
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
177
hesabixCore/src/Entity/PlugHrmAttendance.php
Normal file
177
hesabixCore/src/Entity/PlugHrmAttendance.php
Normal file
|
@ -0,0 +1,177 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\PlugHrmAttendanceRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Ignore;
|
||||
|
||||
#[ORM\Entity(repositoryClass: PlugHrmAttendanceRepository::class)]
|
||||
class PlugHrmAttendance
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'plugHrmAttendances')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[Ignore]
|
||||
private ?Business $business = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[Ignore]
|
||||
private ?Person $person = null;
|
||||
|
||||
#[ORM\Column(length: 10)]
|
||||
private ?string $date = null; // تاریخ شمسی YYYY/MM/DD
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: true)]
|
||||
private ?int $totalHours = 0; // ساعات کل کار (به دقیقه)
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: true)]
|
||||
private ?int $overtimeHours = 0; // ساعات اضافهکاری (به دقیقه)
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
|
||||
private ?\DateTimeInterface $createdAt = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
|
||||
private ?\DateTimeInterface $updatedAt = null;
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'attendance', targetEntity: PlugHrmAttendanceItem::class, orphanRemoval: true)]
|
||||
private Collection $items;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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<int, PlugHrmAttendanceItem>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
99
hesabixCore/src/Entity/PlugHrmAttendanceItem.php
Normal file
99
hesabixCore/src/Entity/PlugHrmAttendanceItem.php
Normal file
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\PlugHrmAttendanceItemRepository;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Ignore;
|
||||
|
||||
#[ORM\Entity(repositoryClass: PlugHrmAttendanceItemRepository::class)]
|
||||
class PlugHrmAttendanceItem
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'items')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[Ignore]
|
||||
private ?PlugHrmAttendance $attendance = null;
|
||||
|
||||
#[ORM\Column(length: 10)]
|
||||
private ?string $type = null; // ورود یا خروج
|
||||
|
||||
#[ORM\Column(length: 5)]
|
||||
private ?string $time = null; // زمان HH:MM
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER)]
|
||||
private ?int $timestamp = null; // unix timestamp
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
|
||||
private ?\DateTimeInterface $createdAt = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\PlugHrmAttendanceItem;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<PlugHrmAttendanceItem>
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
}
|
98
hesabixCore/src/Repository/PlugHrmAttendanceRepository.php
Normal file
98
hesabixCore/src/Repository/PlugHrmAttendanceRepository.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\PlugHrmAttendance;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<PlugHrmAttendance>
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
}
|
|
@ -1082,6 +1082,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',
|
||||
|
|
|
@ -889,6 +889,19 @@ export default {
|
|||
</v-tooltip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item to="/acc/hrm/attendance/list">
|
||||
<v-list-item-title>
|
||||
تردد پرسنل
|
||||
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/hrm/attendance/list') }}</span>
|
||||
</v-list-item-title>
|
||||
<template v-slot:append v-if="permissions.plugHrmAttendance">
|
||||
<v-tooltip text="افزودن تردد جدید" location="end">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon="mdi-plus-box" variant="plain" to="/acc/hrm/attendance/mod/0" />
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list-group>
|
||||
<v-list-group v-show="isPluginActive('ghesta') && permissions.plugGhestaManager">
|
||||
<template v-slot:activator="{ props }">
|
||||
|
|
333
webUI/src/views/acc/plugins/hrm/attendance/list.vue
Normal file
333
webUI/src/views/acc/plugins/hrm/attendance/list.vue
Normal file
|
@ -0,0 +1,333 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-toolbar color="primary" dark>
|
||||
<v-toolbar-title>
|
||||
<v-icon start icon="mdi-clock-check"></v-icon>
|
||||
مدیریت تردد پرسنل
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="permissions.plugHrmAttendance" prepend-icon="mdi-plus" @click="$router.push('/acc/hrm/attendance/mod/0')">
|
||||
افزودن تردد
|
||||
</v-btn>
|
||||
<v-btn prepend-icon="mdi-upload" @click="showImportDialog = true">
|
||||
واردات از اکسل
|
||||
</v-btn>
|
||||
<v-btn prepend-icon="mdi-download" @click="exportData">
|
||||
خروجی اکسل
|
||||
</v-btn>
|
||||
<v-btn prepend-icon="mdi-chart-line" @click="$router.push('/acc/hrm/attendance/reports')">
|
||||
گزارشات
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-container fluid class="pa-4">
|
||||
<!-- فیلترها -->
|
||||
<v-card class="mb-4">
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="3">
|
||||
<Hdatepicker v-model="filters.fromDate" label="از تاریخ" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<Hdatepicker v-model="filters.toDate" label="تا تاریخ" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-select
|
||||
v-model="filters.personId"
|
||||
:items="employees"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
label="پرسنل"
|
||||
clearable
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3" class="d-flex align-center">
|
||||
<v-btn color="primary" @click="loadData" :loading="loading">
|
||||
<v-icon start>mdi-magnify</v-icon>
|
||||
جستجو
|
||||
</v-btn>
|
||||
<v-btn class="ms-2" @click="clearFilters">
|
||||
<v-icon start>mdi-refresh</v-icon>
|
||||
پاک کردن
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- جدول -->
|
||||
<v-card>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="attendances"
|
||||
:loading="loading"
|
||||
:items-per-page="filters.limit"
|
||||
:page="filters.page"
|
||||
:server-items-length="total"
|
||||
@update:options="handleTableUpdate"
|
||||
class="elevation-1"
|
||||
>
|
||||
<template v-slot:item.date="{ item }">
|
||||
<span>{{ formatDate(item.date) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.totalHours="{ item }">
|
||||
<span>{{ formatMinutesToHours(item.totalHours) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.overtimeHours="{ item }">
|
||||
<span>{{ formatMinutesToHours(item.overtimeHours) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.createdAt="{ item }">
|
||||
<span>{{ item.createdAt }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
v-if="permissions.plugHrmAttendance"
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="primary"
|
||||
@click="$router.push(`/acc/hrm/attendance/mod/${item.id}`)"
|
||||
/>
|
||||
<v-btn
|
||||
v-if="permissions.plugHrmAttendance"
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="deleteAttendance(item)"
|
||||
/>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</v-container>
|
||||
|
||||
<!-- دیالوگ واردات -->
|
||||
<v-dialog v-model="showImportDialog" max-width="500">
|
||||
<v-card>
|
||||
<v-card-title>واردات از فایل اکسل</v-card-title>
|
||||
<v-card-text>
|
||||
<v-file-input
|
||||
v-model="importFile"
|
||||
label="انتخاب فایل اکسل"
|
||||
accept=".xlsx,.xls"
|
||||
prepend-icon="mdi-file-excel"
|
||||
show-size
|
||||
/>
|
||||
<v-alert type="info" variant="tonal" class="mt-2">
|
||||
<strong>فرمت فایل:</strong><br>
|
||||
ستون A: کد پرسنل<br>
|
||||
ستون B: تاریخ (YYYY/MM/DD)<br>
|
||||
ستون C: زمان (HH:MM)<br>
|
||||
ستون D: نوع (ورود/خروج)
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn @click="showImportDialog = false">انصراف</v-btn>
|
||||
<v-btn color="primary" @click="importData" :loading="importing">
|
||||
واردات
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- اسنکبار -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import Hdatepicker from '@/components/forms/Hdatepicker.vue';
|
||||
|
||||
export default {
|
||||
name: 'AttendanceList',
|
||||
components: {
|
||||
Hdatepicker
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
importing: false,
|
||||
showImportDialog: false,
|
||||
importFile: null,
|
||||
attendances: [],
|
||||
employees: [],
|
||||
total: 0,
|
||||
permissions: {},
|
||||
filters: {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
fromDate: '',
|
||||
toDate: '',
|
||||
personId: null
|
||||
},
|
||||
headers: [
|
||||
{ title: 'تاریخ', value: 'date', sortable: true },
|
||||
{ title: 'نام پرسنل', value: 'personName', sortable: true },
|
||||
{ title: 'ساعات کل کار', value: 'totalHours', sortable: true },
|
||||
{ title: 'ساعات اضافهکاری', value: 'overtimeHours', sortable: true },
|
||||
{ title: 'توضیحات', value: 'description', sortable: false },
|
||||
{ title: 'تاریخ ثبت', value: 'createdAt', sortable: true },
|
||||
{ title: 'عملیات', value: 'actions', sortable: false, width: '120px' }
|
||||
],
|
||||
snackbar: {
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success'
|
||||
}
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadPermissions();
|
||||
await this.loadEmployees();
|
||||
await this.loadData();
|
||||
},
|
||||
methods: {
|
||||
async loadPermissions() {
|
||||
try {
|
||||
const response = await axios.post('/api/business/get/user/permissions');
|
||||
this.permissions = response.data;
|
||||
} catch (error) {
|
||||
console.error('Error loading permissions:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async loadEmployees() {
|
||||
try {
|
||||
const response = await axios.post('/api/hrm/attendance/employees');
|
||||
this.employees = response.data;
|
||||
} catch (error) {
|
||||
console.error('Error loading employees:', error);
|
||||
this.showSnackbar('خطا در بارگذاری لیست پرسنل', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await axios.post('/api/hrm/attendance/list', this.filters);
|
||||
this.attendances = response.data.data;
|
||||
this.total = response.data.total;
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
this.showSnackbar('خطا در بارگذاری اطلاعات', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteAttendance(item) {
|
||||
if (!confirm(`آیا از حذف تردد ${item.personName} برای تاریخ ${this.formatDate(item.date)} اطمینان دارید؟`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.post(`/api/hrm/attendance/delete/${item.id}`);
|
||||
this.showSnackbar('تردد با موفقیت حذف شد', 'success');
|
||||
await this.loadData();
|
||||
} catch (error) {
|
||||
console.error('Error deleting attendance:', error);
|
||||
this.showSnackbar('خطا در حذف تردد', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async importData() {
|
||||
if (!this.importFile) {
|
||||
this.showSnackbar('لطفاً فایل را انتخاب کنید', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.importing = true;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', this.importFile);
|
||||
|
||||
const response = await axios.post('/api/hrm/attendance/import', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.result === 1) {
|
||||
this.showSnackbar(`${response.data.importedCount} رکورد با موفقیت وارد شد`, 'success');
|
||||
if (response.data.errors && response.data.errors.length > 0) {
|
||||
console.warn('Import errors:', response.data.errors);
|
||||
}
|
||||
this.showImportDialog = false;
|
||||
this.importFile = null;
|
||||
await this.loadData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error importing data:', error);
|
||||
this.showSnackbar('خطا در واردات فایل', 'error');
|
||||
} finally {
|
||||
this.importing = false;
|
||||
}
|
||||
},
|
||||
|
||||
async exportData() {
|
||||
try {
|
||||
const response = await axios.post('/api/hrm/attendance/export', this.filters);
|
||||
if (response.data.result === 1) {
|
||||
// دانلود فایل
|
||||
const link = document.createElement('a');
|
||||
link.href = response.data.downloadUrl;
|
||||
link.download = response.data.filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
this.showSnackbar('فایل با موفقیت دانلود شد', 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error exporting data:', error);
|
||||
this.showSnackbar('خطا در خروجی فایل', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
handleTableUpdate(options) {
|
||||
this.filters.page = options.page;
|
||||
this.filters.limit = options.itemsPerPage;
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
clearFilters() {
|
||||
this.filters = {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
fromDate: '',
|
||||
toDate: '',
|
||||
personId: null
|
||||
};
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
formatDate(date) {
|
||||
if (!date) return '';
|
||||
return date.replace(/\//g, '/');
|
||||
},
|
||||
|
||||
formatMinutesToHours(minutes) {
|
||||
if (!minutes) return '0:00';
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
return `${hours}:${mins.toString().padStart(2, '0')}`;
|
||||
},
|
||||
|
||||
showSnackbar(text, color = 'success') {
|
||||
this.snackbar = {
|
||||
show: true,
|
||||
text,
|
||||
color
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
367
webUI/src/views/acc/plugins/hrm/attendance/mod.vue
Normal file
367
webUI/src/views/acc/plugins/hrm/attendance/mod.vue
Normal file
|
@ -0,0 +1,367 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-toolbar color="primary" dark>
|
||||
<v-toolbar-title>
|
||||
<v-icon start icon="mdi-clock-check"></v-icon>
|
||||
{{ isEdit ? 'ویرایش تردد' : 'افزودن تردد جدید' }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn prepend-icon="mdi-arrow-left" @click="$router.push('/acc/hrm/attendance/list')">
|
||||
بازگشت
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-container fluid class="pa-4">
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<v-form ref="form" @submit.prevent="save">
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-select
|
||||
v-model="form.personId"
|
||||
:items="employees"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
label="پرسنل *"
|
||||
required
|
||||
prepend-inner-icon="mdi-account"
|
||||
:rules="[v => !!v || 'انتخاب پرسنل الزامی است']"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<Hdatepicker
|
||||
v-model="form.date"
|
||||
label="تاریخ *"
|
||||
:rules="[v => !!v || 'تاریخ الزامی است']"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-textarea
|
||||
v-model="form.description"
|
||||
label="توضیحات"
|
||||
rows="1"
|
||||
prepend-inner-icon="mdi-text"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- آیتمهای تردد -->
|
||||
<v-card class="mt-4" variant="outlined">
|
||||
<v-card-title class="text-h6">
|
||||
<v-icon start>mdi-clock-outline</v-icon>
|
||||
ورود و خروج
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="addItem"
|
||||
size="small"
|
||||
>
|
||||
افزودن
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row v-for="(item, index) in form.items" :key="index" class="mb-2">
|
||||
<v-col cols="12" md="3">
|
||||
<v-select
|
||||
v-model="item.type"
|
||||
:items="typeOptions"
|
||||
label="نوع"
|
||||
required
|
||||
:rules="[v => !!v || 'نوع الزامی است']"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-text-field
|
||||
v-model="item.time"
|
||||
label="زمان (HH:MM)"
|
||||
type="time"
|
||||
required
|
||||
:rules="[v => !!v || 'زمان الزامی است']"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="5">
|
||||
<v-text-field
|
||||
v-model="item.note"
|
||||
label="یادداشت"
|
||||
placeholder="توضیحات اختیاری"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="1">
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
color="error"
|
||||
variant="text"
|
||||
@click="removeItem(index)"
|
||||
:disabled="form.items.length <= 1"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- خلاصه -->
|
||||
<v-card class="mt-4" variant="outlined">
|
||||
<v-card-title class="text-h6">
|
||||
<v-icon start>mdi-calculator</v-icon>
|
||||
خلاصه ساعات کار
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card variant="tonal" color="primary">
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h6">{{ formatMinutesToHours(summary.totalHours) }}</div>
|
||||
<div class="text-caption">ساعات کل کار</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card variant="tonal" color="warning">
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h6">{{ formatMinutesToHours(summary.overtimeHours) }}</div>
|
||||
<div class="text-caption">ساعات اضافهکاری</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card variant="tonal" color="success">
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h6">{{ summary.entries }}</div>
|
||||
<div class="text-caption">تعداد ورود</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card variant="tonal" color="info">
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h6">{{ summary.exits }}</div>
|
||||
<div class="text-caption">تعداد خروج</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-row class="mt-4">
|
||||
<v-col cols="12" class="text-center">
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
size="large"
|
||||
:loading="saving"
|
||||
prepend-icon="mdi-content-save"
|
||||
>
|
||||
{{ isEdit ? 'ویرایش' : 'ثبت' }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="ms-2"
|
||||
size="large"
|
||||
@click="$router.push('/acc/hrm/attendance/list')"
|
||||
>
|
||||
انصراف
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-container>
|
||||
|
||||
<!-- اسنکبار -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import Hdatepicker from '@/components/forms/Hdatepicker.vue';
|
||||
|
||||
export default {
|
||||
name: 'AttendanceMod',
|
||||
components: {
|
||||
Hdatepicker
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEdit: false,
|
||||
saving: false,
|
||||
employees: [],
|
||||
form: {
|
||||
personId: null,
|
||||
date: '',
|
||||
description: '',
|
||||
items: [
|
||||
{
|
||||
type: 'ورود',
|
||||
time: '08:00',
|
||||
note: ''
|
||||
}
|
||||
]
|
||||
},
|
||||
typeOptions: [
|
||||
{ title: 'ورود', value: 'ورود' },
|
||||
{ title: 'خروج', value: 'خروج' }
|
||||
],
|
||||
snackbar: {
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success'
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
summary() {
|
||||
const entries = this.form.items.filter(item => item.type === 'ورود').length;
|
||||
const exits = this.form.items.filter(item => item.type === 'خروج').length;
|
||||
|
||||
// محاسبه ساعات کار (ساده)
|
||||
let totalMinutes = 0;
|
||||
const standardWorkMinutes = 8 * 60; // 8 ساعت
|
||||
|
||||
if (entries > 0 && exits > 0) {
|
||||
// محاسبه ساده: فرض بر این است که اولین ورود و آخرین خروج
|
||||
const sortedItems = [...this.form.items].sort((a, b) => a.time.localeCompare(b.time));
|
||||
const firstEntry = sortedItems.find(item => item.type === 'ورود');
|
||||
const lastExit = sortedItems.reverse().find(item => item.type === 'خروج');
|
||||
|
||||
if (firstEntry && lastExit) {
|
||||
const entryTime = this.timeToMinutes(firstEntry.time);
|
||||
const exitTime = this.timeToMinutes(lastExit.time);
|
||||
totalMinutes = exitTime - entryTime;
|
||||
}
|
||||
}
|
||||
|
||||
const overtimeMinutes = totalMinutes > standardWorkMinutes ? totalMinutes - standardWorkMinutes : 0;
|
||||
const regularMinutes = totalMinutes > standardWorkMinutes ? standardWorkMinutes : totalMinutes;
|
||||
|
||||
return {
|
||||
totalHours: regularMinutes,
|
||||
overtimeHours: overtimeMinutes,
|
||||
entries,
|
||||
exits
|
||||
};
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadEmployees();
|
||||
await this.loadData();
|
||||
},
|
||||
methods: {
|
||||
async loadEmployees() {
|
||||
try {
|
||||
const response = await axios.post('/api/hrm/attendance/employees');
|
||||
this.employees = response.data;
|
||||
} catch (error) {
|
||||
console.error('Error loading employees:', error);
|
||||
this.showSnackbar('خطا در بارگذاری لیست پرسنل', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
const id = this.$route.params.id;
|
||||
if (id && id !== '0') {
|
||||
this.isEdit = true;
|
||||
try {
|
||||
const response = await axios.post(`/api/hrm/attendance/get/${id}`);
|
||||
const data = response.data;
|
||||
|
||||
this.form.personId = data.personId;
|
||||
this.form.date = data.date;
|
||||
this.form.description = data.description;
|
||||
this.form.items = data.items.map(item => ({
|
||||
type: item.type,
|
||||
time: item.time,
|
||||
note: ''
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
this.showSnackbar('خطا در بارگذاری اطلاعات', 'error');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addItem() {
|
||||
this.form.items.push({
|
||||
type: 'ورود',
|
||||
time: '08:00',
|
||||
note: ''
|
||||
});
|
||||
},
|
||||
|
||||
removeItem(index) {
|
||||
if (this.form.items.length > 1) {
|
||||
this.form.items.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
async save() {
|
||||
const { valid } = await this.$refs.form.validate();
|
||||
if (!valid) return;
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const id = this.$route.params.id;
|
||||
const url = id && id !== '0' ? `/api/hrm/attendance/mod/${id}` : '/api/hrm/attendance/mod/0';
|
||||
|
||||
// تبدیل آیتمها به فرمت مورد نیاز
|
||||
const items = this.form.items.map(item => ({
|
||||
type: item.type,
|
||||
time: item.time,
|
||||
timestamp: this.getTimestamp(this.form.date, item.time)
|
||||
}));
|
||||
|
||||
const payload = {
|
||||
...this.form,
|
||||
items
|
||||
};
|
||||
|
||||
const response = await axios.post(url, payload);
|
||||
|
||||
if (response.data.result === 1) {
|
||||
this.showSnackbar(
|
||||
this.isEdit ? 'تردد با موفقیت ویرایش شد' : 'تردد با موفقیت ثبت شد',
|
||||
'success'
|
||||
);
|
||||
this.$router.push('/acc/hrm/attendance/list');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving data:', error);
|
||||
this.showSnackbar('خطا در ذخیره اطلاعات', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
timeToMinutes(time) {
|
||||
const [hours, minutes] = time.split(':').map(Number);
|
||||
return hours * 60 + minutes;
|
||||
},
|
||||
|
||||
getTimestamp(date, time) {
|
||||
// تبدیل تاریخ و زمان شمسی به timestamp
|
||||
const dateTime = `${date} ${time}`;
|
||||
// اینجا باید از سرویس Jdate استفاده شود
|
||||
return Math.floor(Date.now() / 1000); // فعلاً timestamp فعلی
|
||||
},
|
||||
|
||||
formatMinutesToHours(minutes) {
|
||||
if (!minutes) return '0:00';
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
return `${hours}:${mins.toString().padStart(2, '0')}`;
|
||||
},
|
||||
|
||||
showSnackbar(text, color = 'success') {
|
||||
this.snackbar = {
|
||||
show: true,
|
||||
text,
|
||||
color
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
314
webUI/src/views/acc/plugins/hrm/attendance/reports.vue
Normal file
314
webUI/src/views/acc/plugins/hrm/attendance/reports.vue
Normal file
|
@ -0,0 +1,314 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-toolbar color="primary" dark>
|
||||
<v-toolbar-title>
|
||||
<v-icon start icon="mdi-chart-line"></v-icon>
|
||||
گزارشات تردد پرسنل
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn prepend-icon="mdi-arrow-right" @click="$router.back()">
|
||||
بازگشت
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-container fluid class="pa-4">
|
||||
<!-- فیلترها -->
|
||||
<v-card class="mb-4">
|
||||
<v-card-title>فیلترهای گزارش</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="3">
|
||||
<v-select
|
||||
v-model="reportType"
|
||||
:items="reportTypes"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
label="نوع گزارش"
|
||||
prepend-inner-icon="mdi-chart-bar"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<Hdatepicker v-model="filters.fromDate" label="از تاریخ" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<Hdatepicker v-model="filters.toDate" label="تا تاریخ" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-select
|
||||
v-model="filters.personId"
|
||||
:items="employees"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
label="پرسنل"
|
||||
clearable
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" class="d-flex justify-center">
|
||||
<v-btn color="primary" @click="generateReport" :loading="loading" class="me-2">
|
||||
<v-icon start>mdi-chart-line</v-icon>
|
||||
تولید گزارش
|
||||
</v-btn>
|
||||
<v-btn @click="exportReport" :loading="exporting" :disabled="!reportData.length">
|
||||
<v-icon start>mdi-download</v-icon>
|
||||
خروجی اکسل
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- خلاصه آمار -->
|
||||
<v-row v-if="summary" class="mb-4">
|
||||
<v-col cols="12" md="3">
|
||||
<v-card color="primary" dark>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h4">{{ summary.totalDays }}</div>
|
||||
<div class="text-subtitle-1">کل روزهای کاری</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card color="success" dark>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h4">{{ formatMinutesToHours(summary.totalHours) }}</div>
|
||||
<div class="text-subtitle-1">کل ساعات کار</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card color="warning" dark>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h4">{{ formatMinutesToHours(summary.totalOvertime) }}</div>
|
||||
<div class="text-subtitle-1">کل اضافهکاری</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card color="info" dark>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h4">{{ summary.averageHours }}</div>
|
||||
<div class="text-subtitle-1">میانگین ساعات روزانه</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- جدول گزارش -->
|
||||
<v-card v-if="reportData.length > 0">
|
||||
<v-card-title>{{ getReportTitle() }}</v-card-title>
|
||||
<v-data-table
|
||||
:headers="getTableHeaders()"
|
||||
:items="reportData"
|
||||
:loading="loading"
|
||||
class="elevation-1"
|
||||
>
|
||||
<template v-slot:item.date="{ item }">
|
||||
<span>{{ formatDate(item.date) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.totalHours="{ item }">
|
||||
<span>{{ formatMinutesToHours(item.totalHours) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.overtimeHours="{ item }">
|
||||
<span>{{ formatMinutesToHours(item.overtimeHours) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.status="{ item }">
|
||||
<v-chip
|
||||
:color="getStatusColor(item.status)"
|
||||
size="small"
|
||||
>
|
||||
{{ item.status }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
|
||||
<!-- پیام خالی -->
|
||||
<v-card v-else-if="!loading" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1" class="mb-4">mdi-chart-line</v-icon>
|
||||
<div class="text-h6 text-grey">هیچ دادهای برای نمایش وجود ندارد</div>
|
||||
<div class="text-body-2 text-grey-lighten-1">لطفاً فیلترها را تنظیم کرده و گزارش را تولید کنید</div>
|
||||
</v-card>
|
||||
</v-container>
|
||||
|
||||
<!-- اسنکبار -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import Hdatepicker from '@/components/forms/Hdatepicker.vue';
|
||||
|
||||
export default {
|
||||
name: 'AttendanceReports',
|
||||
components: {
|
||||
Hdatepicker
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
exporting: false,
|
||||
reportType: 'daily',
|
||||
reportTypes: [
|
||||
{ title: 'گزارش روزانه', value: 'daily' },
|
||||
{ title: 'گزارش ماهانه', value: 'monthly' },
|
||||
{ title: 'گزارش اضافهکاری', value: 'overtime' },
|
||||
{ title: 'گزارش تاخیر و غیبت', value: 'absence' }
|
||||
],
|
||||
filters: {
|
||||
fromDate: '',
|
||||
toDate: '',
|
||||
personId: null
|
||||
},
|
||||
employees: [],
|
||||
reportData: [],
|
||||
summary: null,
|
||||
snackbar: {
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success'
|
||||
}
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadEmployees();
|
||||
},
|
||||
methods: {
|
||||
async loadEmployees() {
|
||||
try {
|
||||
const response = await axios.post('/api/hrm/attendance/employees');
|
||||
this.employees = response.data;
|
||||
} catch (error) {
|
||||
console.error('Error loading employees:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async generateReport() {
|
||||
if (!this.filters.fromDate || !this.filters.toDate) {
|
||||
this.showSnackbar('لطفاً بازه زمانی را مشخص کنید', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
const response = await axios.post('/api/hrm/attendance/reports', {
|
||||
type: this.reportType,
|
||||
fromDate: this.filters.fromDate,
|
||||
toDate: this.filters.toDate,
|
||||
personId: this.filters.personId
|
||||
});
|
||||
|
||||
if (response.data.result === 1) {
|
||||
this.reportData = response.data.data;
|
||||
this.summary = response.data.summary;
|
||||
} else {
|
||||
this.showSnackbar('خطا در تولید گزارش', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating report:', error);
|
||||
this.showSnackbar('خطا در تولید گزارش', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async exportReport() {
|
||||
try {
|
||||
this.exporting = true;
|
||||
const response = await axios.post('/api/hrm/attendance/export-report', {
|
||||
type: this.reportType,
|
||||
fromDate: this.filters.fromDate,
|
||||
toDate: this.filters.toDate,
|
||||
personId: this.filters.personId
|
||||
});
|
||||
|
||||
if (response.data.result === 1) {
|
||||
// دانلود فایل
|
||||
const link = document.createElement('a');
|
||||
link.href = response.data.downloadUrl;
|
||||
link.download = response.data.filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
this.showSnackbar('فایل با موفقیت دانلود شد', 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error exporting report:', error);
|
||||
this.showSnackbar('خطا در خروجی فایل', 'error');
|
||||
} finally {
|
||||
this.exporting = false;
|
||||
}
|
||||
},
|
||||
|
||||
getReportTitle() {
|
||||
const typeMap = {
|
||||
daily: 'گزارش روزانه تردد',
|
||||
monthly: 'گزارش ماهانه تردد',
|
||||
overtime: 'گزارش اضافهکاری',
|
||||
absence: 'گزارش تاخیر و غیبت'
|
||||
};
|
||||
return typeMap[this.reportType] || 'گزارش تردد';
|
||||
},
|
||||
|
||||
getTableHeaders() {
|
||||
const baseHeaders = [
|
||||
{ title: 'تاریخ', value: 'date', sortable: true },
|
||||
{ title: 'نام پرسنل', value: 'personName', sortable: true },
|
||||
{ title: 'ساعات کل کار', value: 'totalHours', sortable: true },
|
||||
{ title: 'ساعات اضافهکاری', value: 'overtimeHours', sortable: true }
|
||||
];
|
||||
|
||||
if (this.reportType === 'absence') {
|
||||
baseHeaders.push(
|
||||
{ title: 'وضعیت', value: 'status', sortable: true },
|
||||
{ title: 'توضیحات', value: 'description', sortable: false }
|
||||
);
|
||||
}
|
||||
|
||||
return baseHeaders;
|
||||
},
|
||||
|
||||
getStatusColor(status) {
|
||||
const colorMap = {
|
||||
'حضور کامل': 'success',
|
||||
'تاخیر': 'warning',
|
||||
'غیبت': 'error',
|
||||
'نیمه وقت': 'info'
|
||||
};
|
||||
return colorMap[status] || 'default';
|
||||
},
|
||||
|
||||
formatDate(date) {
|
||||
if (!date) return '';
|
||||
return date;
|
||||
},
|
||||
|
||||
formatMinutesToHours(minutes) {
|
||||
if (!minutes) return '0 ساعت';
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
return `${hours} ساعت و ${mins} دقیقه`;
|
||||
},
|
||||
|
||||
showSnackbar(text, color = 'success') {
|
||||
this.snackbar = {
|
||||
show: true,
|
||||
text,
|
||||
color
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-data-table {
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
195
webUI/src/views/acc/plugins/hrm/attendance/view.vue
Normal file
195
webUI/src/views/acc/plugins/hrm/attendance/view.vue
Normal file
|
@ -0,0 +1,195 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-toolbar color="primary" dark>
|
||||
<v-toolbar-title>
|
||||
<v-icon start icon="mdi-eye"></v-icon>
|
||||
مشاهده جزئیات تردد
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="permissions.plugHrmAttendance" prepend-icon="mdi-pencil" @click="$router.push(`/acc/hrm/attendance/mod/${attendance.id}`)">
|
||||
ویرایش
|
||||
</v-btn>
|
||||
<v-btn prepend-icon="mdi-arrow-right" @click="$router.back()">
|
||||
بازگشت
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-container fluid class="pa-4" v-if="!loading">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-card>
|
||||
<v-card-title>اطلاعات کلی</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-account"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title>نام پرسنل</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ attendance.personName }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-calendar"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title>تاریخ</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formatDate(attendance.date) }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-clock"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title>ساعات کل کار</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formatMinutesToHours(attendance.totalHours) }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-clock-plus"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title>ساعات اضافهکاری</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formatMinutesToHours(attendance.overtimeHours) }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="attendance.description">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-text"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title>توضیحات</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ attendance.description }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-card>
|
||||
<v-card-title>جزئیات ورود و خروج</v-card-title>
|
||||
<v-card-text>
|
||||
<v-timeline v-if="attendance.items && attendance.items.length > 0">
|
||||
<v-timeline-item
|
||||
v-for="(item, index) in attendance.items"
|
||||
:key="index"
|
||||
:color="item.type === 'ورود' ? 'success' : 'error'"
|
||||
:icon="item.type === 'ورود' ? 'mdi-login' : 'mdi-logout'"
|
||||
>
|
||||
<template v-slot:opposite>
|
||||
<span class="text-caption">{{ item.time }}</span>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon :color="item.type === 'ورود' ? 'success' : 'error'" class="me-2">
|
||||
{{ item.type === 'ورود' ? 'mdi-login' : 'mdi-logout' }}
|
||||
</v-icon>
|
||||
<span class="font-weight-medium">{{ item.type }}</span>
|
||||
</div>
|
||||
<div class="text-caption mt-1">
|
||||
ثبت شده در: {{ formatDateTime(item.createdAt) }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-timeline-item>
|
||||
</v-timeline>
|
||||
<v-alert v-else type="info" variant="tonal">
|
||||
هیچ رکورد ورود و خروجی ثبت نشده است.
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-container v-else class="d-flex justify-center align-center" style="height: 400px;">
|
||||
<v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
|
||||
</v-container>
|
||||
|
||||
<!-- اسنکبار -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: 'AttendanceView',
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
attendance: {},
|
||||
permissions: {},
|
||||
snackbar: {
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success'
|
||||
}
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadPermissions();
|
||||
await this.loadData();
|
||||
},
|
||||
methods: {
|
||||
async loadPermissions() {
|
||||
try {
|
||||
const response = await axios.post('/api/business/get/user/permissions');
|
||||
this.permissions = response.data;
|
||||
} catch (error) {
|
||||
console.error('Error loading permissions:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
try {
|
||||
this.loading = true;
|
||||
const response = await axios.post('/api/hrm/attendance/get/' + this.$route.params.id);
|
||||
|
||||
if (response.data.result === 1) {
|
||||
this.attendance = response.data.data;
|
||||
} else {
|
||||
this.showSnackbar('خطا در بارگذاری اطلاعات', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading attendance data:', error);
|
||||
this.showSnackbar('خطا در بارگذاری اطلاعات', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
formatDate(date) {
|
||||
if (!date) return '';
|
||||
return date;
|
||||
},
|
||||
|
||||
formatMinutesToHours(minutes) {
|
||||
if (!minutes) return '0 ساعت';
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
return `${hours} ساعت و ${mins} دقیقه`;
|
||||
},
|
||||
|
||||
formatDateTime(timestamp) {
|
||||
if (!timestamp) return '';
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleString('fa-IR');
|
||||
},
|
||||
|
||||
showSnackbar(text, color = 'success') {
|
||||
this.snackbar = {
|
||||
show: true,
|
||||
text,
|
||||
color
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-timeline-item__opposite {
|
||||
min-width: 80px;
|
||||
}
|
||||
</style>
|
|
@ -549,6 +549,18 @@
|
|||
:disabled="loadingSwitches.plugHrmDocs"
|
||||
></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch
|
||||
v-model="info.plugHrmAttendance"
|
||||
label="مدیریت تردد پرسنل"
|
||||
@change="savePerms('plugHrmAttendance')"
|
||||
hide-details
|
||||
color="success"
|
||||
density="comfortable"
|
||||
:loading="loadingSwitches.plugHrmAttendance"
|
||||
:disabled="loadingSwitches.plugHrmAttendance"
|
||||
></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
@ -792,6 +804,7 @@ export default {
|
|||
plugNoghreSell: false,
|
||||
plugCCAdmin: false,
|
||||
plugHrmDocs: false,
|
||||
plugHrmAttendance: false,
|
||||
plugGhestaManager: false,
|
||||
plugTaxSettings: false,
|
||||
inquiry: false,
|
||||
|
|
Loading…
Reference in a new issue