Files
SIRH/frontend/components/employees/RttTab.vue
tristan 0cc2b2730a
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
feat : ajout des frais kms + alignment du style de l'application
2026-03-13 16:05:54 +01:00

408 lines
18 KiB
Vue

<template>
<section class="flex h-full min-h-0 flex-col overflow-hidden pt-8">
<!-- Header bar -->
<div class="flex items-center justify-between rounded-t-md bg-tertiary-500 px-5 py-4 text-black border border-primary-500">
<div class="flex items-center">
<button
class="rounded px-2 py-1 font-bold hover:bg-primary-600 disabled:opacity-40 disabled:cursor-not-allowed flex items-center"
:disabled="currentMonthIndex === 0"
@click="currentMonthIndex--"
>
<Icon name="mdi:chevron-left" size="24"/>
</button>
<span class="text-lg font-bold tracking-wide min-w-[170px] text-center">
{{ currentMonthLabel }} {{ currentYear }}
</span>
<button
class="rounded px-2 py-1 font-bold hover:bg-primary-600 disabled:opacity-40 disabled:cursor-not-allowed flex items-center"
:disabled="currentMonthIndex === 11"
@click="currentMonthIndex++"
>
<Icon name="mdi:chevron-right" size="24"/>
</button>
</div>
<p class="text-[16px]">
<span class="font-bold">RTT À LA DATE DU JOUR :</span>
{{ formatMinutes(summary?.availableMinutes ?? 0) }}
</p>
<div class="flex justify-center">
<button
class="rounded-md bg-primary-500 px-8 py-2 font-bold text-white hover:bg-primary-600"
@click="openPaymentDrawer"
>
+ Payer les RRT
</button>
</div>
</div>
<!-- Table -->
<div class="min-h-0 flex-1 overflow-y-auto">
<table class="w-full table-fixed border-collapse text-[18px]">
<colgroup>
<col />
<col class="w-[14%]" />
<col class="w-[14%]" />
<col class="w-[14%]" />
<col class="w-[14%]" />
<col class="w-[14%]" />
<col class="w-[14%]" />
</colgroup>
<thead>
<tr>
<th class="px-5 py-[10px] text-left font-bold text-primary-500 border border-primary-500">Semaine</th>
<th class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">Heure</th>
<th class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">Base</th>
<th class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">25%</th>
<th class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">Base</th>
<th class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">50%</th>
<th class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">Total</th>
</tr>
</thead>
<tbody>
<!-- Report row (only on June when carry > 0) -->
<tr v-if="showReportRow">
<td class="px-5 py-[10px] font-bold text-primary-500 border border-primary-500">Report</td>
<td class="px-4 py-[10px] text-center text-neutral-500 border border-primary-500 border-r-2">-</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(summary!.carryBase25Minutes) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(summary!.carryBonus25Minutes) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(summary!.carryBase50Minutes) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(summary!.carryBonus50Minutes) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(summary!.carryFromPreviousYearMinutes) }}</td>
</tr>
<!-- Week rows (always 5) -->
<tr
v-for="(week, idx) in paddedWeeks"
:key="week ? week.weekStart : `empty-${idx}`"
>
<td class="px-5 py-[10px] font-bold text-primary-500 border border-primary-500">
<span v-if="week">Semaine {{ week.weekNumber }}</span>
<span v-else>&nbsp;</span>
</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">
<span v-if="week">{{ formatMinutes(week.overtimeMinutes) }}</span>
<span v-else>0 h</span>
</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">
<span v-if="week">{{ formatMinutes(week.base25Minutes) }}</span>
<span v-else>0 h</span>
</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">
<span v-if="week">{{ formatMinutes(week.bonus25Minutes) }}</span>
<span v-else>0 h</span>
</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">
<span v-if="week">{{ formatMinutes(week.base50Minutes) }}</span>
<span v-else>0 h</span>
</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">
<span v-if="week">{{ formatMinutes(week.bonus50Minutes) }}</span>
<span v-else>0 h</span>
</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">
<span v-if="week">{{ formatMinutes(week.totalMinutes) }}</span>
<span v-else>0 h</span>
</td>
</tr>
<!-- Total row -->
<tr>
<td class="px-5 py-[10px] font-bold text-primary-500 border border-primary-500 border-t-2">Total</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2 border-t-2">{{ formatMinutes(totals.overtime) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-t-2">{{ formatMinutes(totals.base25) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2 border-t-2">{{ formatMinutes(totals.bonus25) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-t-2">{{ formatMinutes(totals.base50) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2 border-t-2">{{ formatMinutes(totals.bonus50) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-t-2">{{ formatMinutes(totals.total) }}</td>
</tr>
<!-- Payé row -->
<tr>
<td class="px-5 py-[10px] font-bold text-primary-500 border border-primary-500">Payé</td>
<td class="px-4 py-[10px] text-center text-neutral-500 border border-primary-500 border-r-2">-</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ currentPayment ? formatMinutes(-currentPayment.paidBase25Minutes) : '0 h' }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ currentPayment ? formatMinutes(-currentPayment.paidBonus25Minutes) : '0 h' }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ currentPayment ? formatMinutes(-currentPayment.paidBase50Minutes) : '0 h' }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ currentPayment ? formatMinutes(-currentPayment.paidBonus50Minutes) : '0 h' }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(paidTotal) }}</td>
</tr>
<!-- Reste row -->
<tr>
<td class="px-5 py-[10px] font-bold text-primary-500 border border-primary-500">Reste</td>
<td class="px-4 py-[10px] text-center text-neutral-500 border border-primary-500 border-r-2">-</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(totals.base25 - (currentPayment?.paidBase25Minutes ?? 0)) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(totals.bonus25 - (currentPayment?.paidBonus25Minutes ?? 0)) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(totals.base50 - (currentPayment?.paidBase50Minutes ?? 0)) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(totals.bonus50 - (currentPayment?.paidBonus50Minutes ?? 0)) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(resteTotal) }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Payment Drawer -->
<AppDrawer v-model="isPaymentDrawerOpen" title="Payer des RTT">
<form @submit.prevent="onSubmitPayment">
<div class="mb-4">
<label class="block text-sm font-medium text-neutral-700">Mois</label>
<select
v-model.number="paymentForm.month"
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-md text-neutral-900"
>
<option v-for="m in orderedMonthOptions" :key="m.value" :value="m.value">{{ m.label }}</option>
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-neutral-700">Base 25% (heures)</label>
<input
v-model.number="paymentForm.base25Hours"
type="number"
step="0.5"
min="0"
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
/>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-neutral-700">Heures 25% (heures)</label>
<input
v-model.number="paymentForm.bonus25Hours"
type="number"
step="0.5"
min="0"
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
/>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-neutral-700">Base 50% (heures)</label>
<input
v-model.number="paymentForm.base50Hours"
type="number"
step="0.5"
min="0"
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
/>
</div>
<div class="mb-6">
<label class="block text-sm font-medium text-neutral-700">Heures 50% (heures)</label>
<input
v-model.number="paymentForm.bonus50Hours"
type="number"
step="0.5"
min="0"
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
/>
</div>
<div class="flex justify-end gap-3">
<button
type="button"
class="rounded-md border border-neutral-300 px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50"
@click="isPaymentDrawerOpen = false"
>
Annuler
</button>
<button
type="submit"
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-medium text-white hover:bg-primary-600"
>
Enregistrer
</button>
</div>
</form>
</AppDrawer>
</section>
</template>
<script setup lang="ts">
import type { EmployeeRttSummary, EmployeeRttWeekSummary } from '~/services/dto/employee-rtt-summary'
import AppDrawer from '~/components/AppDrawer.vue'
const props = defineProps<{
summary: EmployeeRttSummary | null
}>()
const emit = defineEmits<{
(event: 'submit-rtt-payment', month: number, base25Minutes: number, bonus25Minutes: number, base50Minutes: number, bonus50Minutes: number): void
}>()
// --- Month navigation ---
const orderedMonths = [6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5] as const
const monthLabels: Record<number, string> = {
1: 'JANVIER',
2: 'FEVRIER',
3: 'MARS',
4: 'AVRIL',
5: 'MAI',
6: 'JUIN',
7: 'JUILLET',
8: 'AOUT',
9: 'SEPTEMBRE',
10: 'OCTOBRE',
11: 'NOVEMBRE',
12: 'DECEMBRE',
}
const orderedMonthOptions = [
{ value: 6, label: 'Juin' },
{ value: 7, label: 'Juillet' },
{ value: 8, label: 'Aout' },
{ value: 9, label: 'Septembre' },
{ value: 10, label: 'Octobre' },
{ value: 11, label: 'Novembre' },
{ value: 12, label: 'Decembre' },
{ value: 1, label: 'Janvier' },
{ value: 2, label: 'Fevrier' },
{ value: 3, label: 'Mars' },
{ value: 4, label: 'Avril' },
{ value: 5, label: 'Mai' },
]
// Initialize to current month's position in the exercise
const today = new Date()
const todayMonth = today.getMonth() + 1
const initialIndex = orderedMonths.indexOf(todayMonth as (typeof orderedMonths)[number])
const currentMonthIndex = ref(initialIndex >= 0 ? initialIndex : 0)
const currentMonth = computed(() => orderedMonths[currentMonthIndex.value])
const currentMonthLabel = computed(() => monthLabels[currentMonth.value])
const currentYear = computed(() => {
if (!props.summary) return ''
return currentMonth.value >= 6 ? props.summary.year - 1 : props.summary.year
})
// --- Weeks for current month ---
const weeksForCurrentMonth = computed((): EmployeeRttWeekSummary[] => {
if (!props.summary) return []
return props.summary.weeks.filter((w) => w.month === currentMonth.value)
})
const paddedWeeks = computed((): (EmployeeRttWeekSummary | null)[] => {
const weeks = weeksForCurrentMonth.value
const padded: (EmployeeRttWeekSummary | null)[] = [...weeks]
while (padded.length < 5) {
padded.push(null)
}
return padded
})
// --- Report row ---
const reportMonth = computed(() => {
if (!props.summary) return 6
const carryMonth = props.summary.carryMonth
// Report appears in the month AFTER carryMonth (wrapping 12 -> 1)
return carryMonth >= 12 ? 1 : carryMonth + 1
})
const showReportRow = computed(() => {
return (
currentMonth.value === reportMonth.value &&
(props.summary?.carryFromPreviousYearMinutes ?? 0) > 0
)
})
// --- Totals ---
const totals = computed(() => {
const weeks = weeksForCurrentMonth.value
const base = {
overtime: weeks.reduce((s, w) => s + w.overtimeMinutes, 0),
base25: weeks.reduce((s, w) => s + w.base25Minutes, 0),
bonus25: weeks.reduce((s, w) => s + w.bonus25Minutes, 0),
base50: weeks.reduce((s, w) => s + w.base50Minutes, 0),
bonus50: weeks.reduce((s, w) => s + w.bonus50Minutes, 0),
total: weeks.reduce((s, w) => s + w.totalMinutes, 0),
}
if (showReportRow.value && props.summary) {
base.base25 += props.summary.carryBase25Minutes
base.bonus25 += props.summary.carryBonus25Minutes
base.base50 += props.summary.carryBase50Minutes
base.bonus50 += props.summary.carryBonus50Minutes
base.total += props.summary.carryFromPreviousYearMinutes
}
return base
})
const currentPayment = computed(() => {
if (!props.summary) return null
return props.summary.monthPayments.find((p) => p.month === currentMonth.value) ?? null
})
const paidTotal = computed(() => {
if (!currentPayment.value) return 0
const p = currentPayment.value
return -(p.paidBase25Minutes + p.paidBonus25Minutes + p.paidBase50Minutes + p.paidBonus50Minutes)
})
const resteTotal = computed(() => {
return totals.value.total + paidTotal.value
})
// --- Format ---
const formatMinutes = (minutes: number): string => {
if (minutes === 0) return '0 h'
const sign = minutes < 0 ? '- ' : ''
const abs = Math.abs(minutes)
const hours = Math.floor(abs / 60)
const rest = abs % 60
if (rest === 0) return `${sign}${hours} h`
return `${sign}${hours} h ${rest} m`
}
// --- Payment drawer ---
const isPaymentDrawerOpen = ref(false)
const paymentForm = reactive({
month: 6,
base25Hours: 0,
bonus25Hours: 0,
base50Hours: 0,
bonus50Hours: 0,
})
const prefillFromExistingPayment = (month: number) => {
const existing = props.summary?.monthPayments.find((p) => p.month === month) ?? null
if (existing) {
paymentForm.base25Hours = existing.paidBase25Minutes / 60
paymentForm.bonus25Hours = existing.paidBonus25Minutes / 60
paymentForm.base50Hours = existing.paidBase50Minutes / 60
paymentForm.bonus50Hours = existing.paidBonus50Minutes / 60
} else {
paymentForm.base25Hours = 0
paymentForm.bonus25Hours = 0
paymentForm.base50Hours = 0
paymentForm.bonus50Hours = 0
}
}
watch(() => paymentForm.month, (newMonth) => {
prefillFromExistingPayment(newMonth)
})
const openPaymentDrawer = () => {
paymentForm.month = currentMonth.value
prefillFromExistingPayment(currentMonth.value)
isPaymentDrawerOpen.value = true
}
const onSubmitPayment = () => {
emit(
'submit-rtt-payment',
paymentForm.month,
Math.round(paymentForm.base25Hours * 60),
Math.round(paymentForm.bonus25Hours * 60),
Math.round(paymentForm.base50Hours * 60),
Math.round(paymentForm.bonus50Hours * 60),
)
isPaymentDrawerOpen.value = false
}
</script>