feat: import work hours from SaaS and exchange signed time sheet

This commit is contained in:
fromme committed 2026-09-26 12:13:59 +02:00
1 parent 7cf39850e6
commit 1a7c272414
73 files changed
+10488 -72

No files matched your search

@@ -0,0 +1,134 @@
<?php
namespace App\Repository;
use App\Entity\Teamer;
use App\Entity\TeamerWorkMonth;
use App\Enum\WorkMonthStatus;
use App\Model\WorkMonthFilterDto;
use App\Repository\Traits\QueryHelperTrait;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<TeamerWorkMonth>
*
* @method TeamerWorkMonth|null find($id, $lockMode = null, $lockVersion = null)
* @method TeamerWorkMonth|null findOneBy(array $criteria, array $orderBy = null)
* @method TeamerWorkMonth[] findAll()
* @method TeamerWorkMonth[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class TeamerWorkMonthRepository extends ServiceEntityRepository
{
use QueryHelperTrait;
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TeamerWorkMonth::class);
}
public function findOneForTeamerAndMonth(Teamer $teamer, int $year, int $month): ?TeamerWorkMonth
{
return $this->findOneBy(['teamer' => $teamer, 'year' => $year, 'month' => $month]);
}
/**
* @return TeamerWorkMonth[]
*/
public function findForTeamer(Teamer $teamer): array
{
return $this->findBy(
['teamer' => $teamer],
['year' => \SortDirection::Descending, 'month' => \SortDirection::Descending],
);
}
/**
* The admin list.
*
* Deliberately over TeamerWorkMonth rather than over Upload: UploadRepository's list
* queries inner-join a disposition, which structurally excludes every document hanging
* off anything else, and it is the month that carries the status anyway.
*/
public function getListQuery(WorkMonthFilterDto $filterDto): Query
{
$qb = $this->createQueryBuilder('work_month');
$qb
->select('work_month', 'teamer')
->innerJoin('work_month.teamer', 'teamer')
;
$this->applyFilterSettings($filterDto, $qb);
return $qb
->addOrderBy('work_month.year', \SortDirection::Descending)
->addOrderBy('work_month.month', \SortDirection::Descending)
->addOrderBy('teamer.lastName', \SortDirection::Ascending)
->getQuery()
;
}
/**
* Months still waiting for a signed copy, for the reminder.
*
* @return TeamerWorkMonth[]
*/
public function findAwaitingDocument(\DateTimeImmutable $importedBefore): array
{
return $this
->createQueryBuilder('work_month')
->select('work_month', 'teamer')
->innerJoin('work_month.teamer', 'teamer')
->andWhere('work_month.status IN (:awaiting)')
->andWhere('work_month.syncedAt <= :importedBefore')
->andWhere('teamer.deletedAt IS NULL')
->setParameter('awaiting', [WorkMonthStatus::OPEN, WorkMonthStatus::REJECTED])
->setParameter('importedBefore', $importedBefore)
->addOrderBy('teamer.lastName', \SortDirection::Ascending)
->getQuery()
->getResult()
;
}
private function applyFilterSettings(WorkMonthFilterDto $filterDto, QueryBuilder $qb): void
{
if (null !== $filterDto->getTeamerName()) {
$qb
->andWhere($qb->expr()->orX(
$qb->expr()->like('teamer.firstName', ':teamerName'),
$qb->expr()->like('teamer.lastName', ':teamerName')
))
->setParameter('teamerName', '%'.$this->escapeLikeWildcards($filterDto->getTeamerName()).'%')
;
}
if (null !== $period = $filterDto->getPeriod()) {
$qb
->andWhere('work_month.year = :year')
->andWhere('work_month.month = :month')
->setParameter('year', (int) $period->format('Y'))
->setParameter('month', (int) $period->format('n'))
;
}
if (0 < count($filterDto->getStatuses())) {
$qb
->andWhere('work_month.status IN (:statuses)')
->setParameter('statuses', $filterDto->getStatuses())
;
} elseif (!$filterDto->isIncludeApproved()) {
// Only a default, so an explicit status choice still wins.
$qb
->andWhere('work_month.status != :approved')
->setParameter('approved', WorkMonthStatus::APPROVED)
;
}
if ($filterDto->isIncludeMismatchedOnly()) {
$qb->andWhere('work_month.mismatchDetectedAt IS NOT NULL');
}
}
}