Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c35edb9a1c | ||
| 4b04be1d1b | |||
| b24dd8595d | |||
|
|
96185e2334 | ||
| 7d53000fc2 |
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.1.34'
|
app.version: '0.1.36'
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="isOpen"
|
v-if="isOpen"
|
||||||
class="absolute left-0 top-full z-20 mt-2 max-h-80 w-full overflow-auto rounded-md border border-neutral-200 bg-white p-3 shadow-lg"
|
class="z-50 absolute left-0 top-full z-20 mt-2 max-h-80 w-full overflow-auto rounded-md border border-neutral-200 bg-white p-3 shadow-lg"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label
|
<label
|
||||||
|
|||||||
@@ -138,19 +138,17 @@ export const useHoursPage = () => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const canCreateValidationRowFromAbsence = (employeeId: number) => {
|
const canCreateEmptyValidationRow = (employeeId: number) => {
|
||||||
const row = rows.value[employeeId]
|
const row = rows.value[employeeId]
|
||||||
if (row?.workHourId) return false
|
if (row?.workHourId) return false
|
||||||
|
if (!hasContractAtSelectedDate(employeeId)) return false
|
||||||
const dayRow = dayContextByEmployeeId.value.get(employeeId)
|
const dayRow = dayContextByEmployeeId.value.get(employeeId)
|
||||||
return !!dayRow?.absenceLabel && hasContractAtSelectedDate(employeeId)
|
return !!dayRow?.absenceLabel || is4hContract(employeeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const canCreateSiteValidationRowFromAbsence = (employeeId: number) => {
|
const canCreateValidationRowFromAbsence = (employeeId: number) => canCreateEmptyValidationRow(employeeId)
|
||||||
const row = rows.value[employeeId]
|
|
||||||
if (row?.workHourId) return false
|
const canCreateSiteValidationRowFromAbsence = (employeeId: number) => canCreateEmptyValidationRow(employeeId)
|
||||||
const dayRow = dayContextByEmployeeId.value.get(employeeId)
|
|
||||||
return !!dayRow?.absenceLabel && hasContractAtSelectedDate(employeeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const bulkValidatableEmployeeIds = computed(() => {
|
const bulkValidatableEmployeeIds = computed(() => {
|
||||||
return visibleEmployees.value
|
return visibleEmployees.value
|
||||||
@@ -347,6 +345,10 @@ export const useHoursPage = () => {
|
|||||||
|
|
||||||
const isPresenceTracking = (employee: Employee) => employee.contract?.trackingMode === TRACKING_MODES.PRESENCE
|
const isPresenceTracking = (employee: Employee) => employee.contract?.trackingMode === TRACKING_MODES.PRESENCE
|
||||||
const isTimeTracking = (employee: Employee) => !isPresenceTracking(employee)
|
const isTimeTracking = (employee: Employee) => !isPresenceTracking(employee)
|
||||||
|
const is4hContract = (employeeId: number) => {
|
||||||
|
const employee = employees.value.find((e) => e.id === employeeId)
|
||||||
|
return employee?.contract?.weeklyHours === 4
|
||||||
|
}
|
||||||
const isRowLocked = (employeeId: number) => {
|
const isRowLocked = (employeeId: number) => {
|
||||||
const row = rows.value[employeeId]
|
const row = rows.value[employeeId]
|
||||||
if (!row) return false
|
if (!row) return false
|
||||||
@@ -692,13 +694,8 @@ export const useHoursPage = () => {
|
|||||||
options: { toast?: boolean } = {}
|
options: { toast?: boolean } = {}
|
||||||
) => {
|
) => {
|
||||||
const row = rows.value[employeeId]
|
const row = rows.value[employeeId]
|
||||||
const dayRow = dayContextByEmployeeId.value.get(employeeId)
|
|
||||||
if (!row?.workHourId && checked) {
|
if (!row?.workHourId && checked) {
|
||||||
const employee = employees.value.find((item) => item.id === employeeId)
|
if (canCreateEmptyValidationRow(employeeId)) {
|
||||||
const hasAbsence = !!dayRow?.absenceLabel
|
|
||||||
const canCreateFromAbsence = !!employee && hasAbsence && hasContractAtSelectedDate(employeeId)
|
|
||||||
|
|
||||||
if (canCreateFromAbsence) {
|
|
||||||
await bulkUpsertWorkHours({
|
await bulkUpsertWorkHours({
|
||||||
workDate: selectedDate.value,
|
workDate: selectedDate.value,
|
||||||
entries: [{
|
entries: [{
|
||||||
@@ -746,13 +743,8 @@ export const useHoursPage = () => {
|
|||||||
options: { toast?: boolean } = {}
|
options: { toast?: boolean } = {}
|
||||||
) => {
|
) => {
|
||||||
const row = rows.value[employeeId]
|
const row = rows.value[employeeId]
|
||||||
const dayRow = dayContextByEmployeeId.value.get(employeeId)
|
|
||||||
if (!row?.workHourId && checked) {
|
if (!row?.workHourId && checked) {
|
||||||
const employee = employees.value.find((item) => item.id === employeeId)
|
if (canCreateEmptyValidationRow(employeeId)) {
|
||||||
const hasAbsence = !!dayRow?.absenceLabel
|
|
||||||
const canCreateFromAbsence = !!employee && hasAbsence && hasContractAtSelectedDate(employeeId)
|
|
||||||
|
|
||||||
if (canCreateFromAbsence) {
|
|
||||||
await bulkUpsertWorkHours({
|
await bulkUpsertWorkHours({
|
||||||
workDate: selectedDate.value,
|
workDate: selectedDate.value,
|
||||||
entries: [{
|
entries: [{
|
||||||
|
|||||||
@@ -191,6 +191,43 @@ final class WorkHourRepository extends ServiceEntityRepository implements WorkHo
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the set of Y-m-d dates where the employee has worked hours on the given dates.
|
||||||
|
*
|
||||||
|
* @param list<string> $dates Y-m-d formatted dates
|
||||||
|
*
|
||||||
|
* @return array<string, true> Y-m-d => true
|
||||||
|
*/
|
||||||
|
public function findWorkedDatesAmong(Employee $employee, array $dates): array
|
||||||
|
{
|
||||||
|
if ([] === $dates) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$placeholders = [];
|
||||||
|
$params = ['employee' => $employee->getId()];
|
||||||
|
foreach (array_values($dates) as $i => $date) {
|
||||||
|
$key = "d{$i}";
|
||||||
|
$placeholders[] = ":{$key}";
|
||||||
|
$params[$key] = $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = sprintf(
|
||||||
|
'SELECT work_date FROM work_hours WHERE employee_id = :employee AND work_date IN (%s) AND (morning_from IS NOT NULL OR afternoon_from IS NOT NULL OR evening_from IS NOT NULL)',
|
||||||
|
implode(', ', $placeholders)
|
||||||
|
);
|
||||||
|
|
||||||
|
$conn = $this->getEntityManager()->getConnection();
|
||||||
|
$rows = $conn->fetchAllAssociative($sql, $params);
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$result[(string) $row['work_date']] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
public function hasPendingSiteValidationForSiteAndDate(int $siteId, DateTimeInterface $date): bool
|
public function hasPendingSiteValidationForSiteAndDate(int $siteId, DateTimeInterface $date): bool
|
||||||
{
|
{
|
||||||
$workDate = DateTimeImmutable::createFromInterface($date);
|
$workDate = DateTimeImmutable::createFromInterface($date);
|
||||||
|
|||||||
@@ -565,16 +565,34 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|||||||
$weekendWorkedDays = $this->workHourRepository->countWeekendWorkedDaysByMonth($employee, $from, $to);
|
$weekendWorkedDays = $this->workHourRepository->countWeekendWorkedDaysByMonth($employee, $from, $to);
|
||||||
$absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $from, $to);
|
$absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $from, $to);
|
||||||
|
|
||||||
// Count absence days per month (0.5 for half-days).
|
// Find which public holidays were actually worked (should count as presence).
|
||||||
|
$workedHolidays = [] !== $publicHolidays
|
||||||
|
? $this->workHourRepository->findWorkedDatesAmong($employee, array_keys($publicHolidays))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Count absence days per month, iterating day by day to handle multi-day absences
|
||||||
|
// and properly distribute across months.
|
||||||
$absenceDaysByMonth = [];
|
$absenceDaysByMonth = [];
|
||||||
foreach ($absences as $absence) {
|
foreach ($absences as $absence) {
|
||||||
$date = DateTimeImmutable::createFromInterface($absence->getStartDate());
|
$start = DateTimeImmutable::createFromInterface($absence->getStartDate())->setTime(0, 0);
|
||||||
$monthKey = $date->format('Y-m');
|
$end = DateTimeImmutable::createFromInterface($absence->getEndDate())->setTime(0, 0);
|
||||||
$days = 1.0;
|
|
||||||
if ($absence->getStartHalf() === $absence->getEndHalf()) {
|
for ($day = $start; $day <= $end; $day = $day->modify('+1 day')) {
|
||||||
$days = 0.5;
|
$weekDay = (int) $day->format('N');
|
||||||
|
// Skip weekends
|
||||||
|
if ($weekDay >= 6) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$monthKey = $day->format('Y-m');
|
||||||
|
[$am, $pm] = $this->resolveSegmentsForDate($absence, $day->format('Y-m-d'));
|
||||||
|
$dayAmount = ($am ? 0.5 : 0.0) + ($pm ? 0.5 : 0.0);
|
||||||
|
if ($dayAmount <= 0.0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$absenceDaysByMonth[$monthKey] = ($absenceDaysByMonth[$monthKey] ?? 0.0) + $dayAmount;
|
||||||
}
|
}
|
||||||
$absenceDaysByMonth[$monthKey] = ($absenceDaysByMonth[$monthKey] ?? 0.0) + $days;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count business days and public holidays per month.
|
// Count business days and public holidays per month.
|
||||||
@@ -595,7 +613,8 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|||||||
$day = $day->modify('+1 day')
|
$day = $day->modify('+1 day')
|
||||||
) {
|
) {
|
||||||
$weekDay = (int) $day->format('N');
|
$weekDay = (int) $day->format('N');
|
||||||
if ($weekDay <= 5 && !isset($publicHolidays[$day->format('Y-m-d')])) {
|
$dayKey = $day->format('Y-m-d');
|
||||||
|
if ($weekDay <= 5 && (!isset($publicHolidays[$dayKey]) || isset($workedHolidays[$dayKey]))) {
|
||||||
++$businessDays;
|
++$businessDays;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,13 +134,15 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$is4hContract = 4 === $contract->getWeeklyHours();
|
||||||
|
|
||||||
if ($this->isEntryEmpty($normalized)) {
|
if ($this->isEntryEmpty($normalized)) {
|
||||||
// Convention choisie: une ligne vide supprime l'enregistrement existant.
|
// Convention choisie: une ligne vide supprime l'enregistrement existant.
|
||||||
if ($existing) {
|
if ($existing) {
|
||||||
$this->entityManager->remove($existing);
|
$this->entityManager->remove($existing);
|
||||||
++$result->deleted;
|
++$result->deleted;
|
||||||
} elseif (($absenceByEmployeeId[$employeeId] ?? false) === true) {
|
} elseif (($absenceByEmployeeId[$employeeId] ?? false) === true || $is4hContract) {
|
||||||
// Si une absence existe ce jour, on garde une ligne technique pour pouvoir valider la journée.
|
// Si une absence existe ce jour ou contrat 4h, on garde une ligne technique pour pouvoir valider la journée.
|
||||||
$workHour = new WorkHour()
|
$workHour = new WorkHour()
|
||||||
->setEmployee($employee)
|
->setEmployee($employee)
|
||||||
->setWorkDate($workDate)
|
->setWorkDate($workDate)
|
||||||
|
|||||||
Reference in New Issue
Block a user