408 lines
18 KiB
Vue
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> </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>
|