feat : modification des exports PDF et affichage du type de contrat sur l'écran des heures
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<AppDrawer v-model="drawerOpen" title="Export heures annuelles">
|
||||
<AppDrawer v-model="drawerOpen" title="Export heures">
|
||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||
<div>
|
||||
<label class="text-md font-semibold text-neutral-700" for="yearly-hours-year">
|
||||
@@ -14,6 +14,20 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-md font-semibold text-neutral-700" for="yearly-hours-month">
|
||||
Mois
|
||||
</label>
|
||||
<select
|
||||
id="yearly-hours-month"
|
||||
v-model="selectedMonth"
|
||||
:class="selectFieldClass"
|
||||
>
|
||||
<option value="">Toute l'année</option>
|
||||
<option v-for="m in months" :key="m.value" :value="m.value">{{ m.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
@@ -37,7 +51,7 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: boolean): void
|
||||
(event: 'submit', year: number): void
|
||||
(event: 'submit', payload: { year: number; month: number | null }): void
|
||||
}>()
|
||||
|
||||
const drawerOpen = computed({
|
||||
@@ -47,13 +61,31 @@ const drawerOpen = computed({
|
||||
|
||||
const currentYear = new Date().getFullYear()
|
||||
const years = Array.from({ length: 6 }, (_, i) => currentYear - i)
|
||||
const months = [
|
||||
{ value: 1, label: 'Janvier' },
|
||||
{ value: 2, label: 'Février' },
|
||||
{ value: 3, label: 'Mars' },
|
||||
{ value: 4, label: 'Avril' },
|
||||
{ value: 5, label: 'Mai' },
|
||||
{ value: 6, label: 'Juin' },
|
||||
{ value: 7, label: 'Juillet' },
|
||||
{ value: 8, label: 'Août' },
|
||||
{ value: 9, label: 'Septembre' },
|
||||
{ value: 10, label: 'Octobre' },
|
||||
{ value: 11, label: 'Novembre' },
|
||||
{ value: 12, label: 'Décembre' }
|
||||
]
|
||||
const selectedYear = ref(currentYear)
|
||||
const selectedMonth = ref<number | ''>('')
|
||||
|
||||
const baseInputClass = 'mt-2 w-full rounded-md border px-3 py-2 text-md text-neutral-900'
|
||||
const selectFieldClass = computed(() => `${baseInputClass} border-neutral-300`)
|
||||
|
||||
const handleSubmit = () => {
|
||||
emit('submit', selectedYear.value)
|
||||
emit('submit', {
|
||||
year: selectedYear.value,
|
||||
month: selectedMonth.value === '' ? null : selectedMonth.value
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -61,6 +93,7 @@ watch(
|
||||
(isOpen) => {
|
||||
if (!isOpen) {
|
||||
selectedYear.value = currentYear
|
||||
selectedMonth.value = ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -42,7 +42,9 @@
|
||||
<span class="font-normal text-neutral-600">({{ contractLabel(employee) }})</span>
|
||||
</p>
|
||||
<p class="text-neutral-500 truncate inline-flex items-center gap-2">
|
||||
<span>{{ employee.site?.name ?? 'Sans site' }}</span>
|
||||
<span>
|
||||
{{ employee.site?.name ?? 'Sans site' }}<span v-if="employee.currentContractNature"> — {{ contractNatureLabel(employee.currentContractNature) }}</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="isAdmin && (rows[employee.id]?.isSiteValid ?? false)"
|
||||
class="rounded-full bg-green-500 flex justify-center item-center text-white p-0.5"
|
||||
@@ -170,6 +172,7 @@
|
||||
import type { Employee } from '~/services/dto/employee'
|
||||
import TimeSelect from '~/components/ui/TimeSelect.vue'
|
||||
import type { DriverHourRow } from '~/services/dto/work-hour'
|
||||
import { contractNatureLabel } from '~/utils/contract'
|
||||
|
||||
const rows = defineModel<Record<number, DriverHourRow>>('rows', { required: true })
|
||||
const bulkValidationInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
{{ row.firstName }} {{ row.lastName }}
|
||||
<span class="font-normal text-neutral-600">({{ row.contractName ?? '-' }})</span>
|
||||
</p>
|
||||
<p class="text-[11px] text-neutral-500 truncate">{{ row.siteName ?? 'Sans site' }}</p>
|
||||
<p class="text-[11px] text-neutral-500 truncate">
|
||||
{{ row.siteName ?? 'Sans site' }}<span v-if="row.contractNature"> — {{ contractNatureLabel(row.contractNature) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -89,6 +91,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { WeeklyWorkHourSummary } from '~/services/dto/work-hour'
|
||||
import { contractNatureLabel } from '~/utils/contract'
|
||||
|
||||
const getDailyCellStyle = (daily: {
|
||||
hasAbsence?: boolean
|
||||
|
||||
113
frontend/components/employees/WorkDaysHoursInput.vue
Normal file
113
frontend/components/employees/WorkDaysHoursInput.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="rounded-md border border-neutral-200 bg-neutral-50 p-3 space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-md font-semibold text-neutral-700">
|
||||
Jours travaillés <span v-if="!disabled" class="text-red-600">*</span>
|
||||
</p>
|
||||
<p class="text-sm" :class="totalIsValid ? 'text-green-700' : 'text-red-600'">
|
||||
{{ formatTotal(totalMinutes) }} / {{ formatTotal(expectedMinutes) }}
|
||||
</p>
|
||||
</div>
|
||||
<p v-if="!disabled" class="text-xs text-neutral-500">Somme requise = {{ expectedMinutes / 60 }}h (total hebdo du contrat).</p>
|
||||
<div class="space-y-1">
|
||||
<div v-for="day in days" :key="day.iso" class="flex items-center gap-3">
|
||||
<label class="inline-flex items-center gap-2 min-w-[120px]">
|
||||
<input
|
||||
:checked="day.active"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-neutral-300 text-primary-500 focus:ring-primary-500"
|
||||
:disabled="disabled"
|
||||
@change="onToggleDay(day.iso, ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
<span class="text-md text-neutral-700">{{ day.label }}</span>
|
||||
</label>
|
||||
<input
|
||||
:value="day.time"
|
||||
type="time"
|
||||
step="60"
|
||||
class="rounded-md border border-neutral-300 bg-white px-2 py-1 text-md text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20 disabled:bg-neutral-100 disabled:text-neutral-400"
|
||||
:disabled="disabled || !day.active"
|
||||
@input="onChangeTime(day.iso, ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!totalIsValid" class="text-sm text-red-600">
|
||||
La somme des heures par jour doit égaler exactement {{ expectedMinutes / 60 }}h.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: Record<number, number> | null
|
||||
contractWeeklyHours: number | null
|
||||
disabled?: boolean
|
||||
}>(), { disabled: false })
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: Record<number, number>]
|
||||
}>()
|
||||
|
||||
const DAY_LABELS: Record<number, string> = { 1: 'Lundi', 2: 'Mardi', 3: 'Mercredi', 4: 'Jeudi', 5: 'Vendredi' }
|
||||
|
||||
const expectedMinutes = computed(() => (props.contractWeeklyHours ?? 0) * 60)
|
||||
|
||||
const days = computed(() => {
|
||||
const raw = props.modelValue ?? {}
|
||||
return [1, 2, 3, 4, 5].map((iso) => {
|
||||
const active = Object.prototype.hasOwnProperty.call(raw, iso)
|
||||
const minutes = Number(raw[iso] ?? 0)
|
||||
return {
|
||||
iso,
|
||||
label: DAY_LABELS[iso],
|
||||
active,
|
||||
time: active ? minutesToTime(minutes) : '00:00',
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const totalMinutes = computed(() => {
|
||||
const raw = props.modelValue ?? {}
|
||||
return Object.values(raw).reduce((sum, n) => sum + (Number(n) || 0), 0)
|
||||
})
|
||||
|
||||
const totalIsValid = computed(() => totalMinutes.value === expectedMinutes.value && expectedMinutes.value > 0)
|
||||
|
||||
function minutesToTime(minutes: number): string {
|
||||
const h = Math.floor(minutes / 60)
|
||||
const m = minutes % 60
|
||||
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function timeToMinutes(value: string): number {
|
||||
const [h, m] = value.split(':').map(Number)
|
||||
return (h || 0) * 60 + (m || 0)
|
||||
}
|
||||
|
||||
function onToggleDay(iso: number, active: boolean) {
|
||||
const next = { ...(props.modelValue ?? {}) }
|
||||
if (active) {
|
||||
next[iso] = next[iso] ?? 0
|
||||
} else {
|
||||
delete next[iso]
|
||||
}
|
||||
emit('update:modelValue', next)
|
||||
}
|
||||
|
||||
function onChangeTime(iso: number, value: string) {
|
||||
const next = { ...(props.modelValue ?? {}) }
|
||||
const minutes = timeToMinutes(value)
|
||||
next[iso] = minutes
|
||||
emit('update:modelValue', next)
|
||||
}
|
||||
|
||||
function formatTotal(min: number): string {
|
||||
const h = Math.floor(min / 60)
|
||||
const m = min % 60
|
||||
return m === 0 ? `${h}h` : `${h}h${String(m).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
defineExpose({ totalIsValid, totalMinutes })
|
||||
</script>
|
||||
@@ -43,7 +43,9 @@
|
||||
<span class="font-normal text-neutral-600">({{ contractLabel(employee) }})</span>
|
||||
</p>
|
||||
<p class="text-neutral-500 truncate inline-flex items-center gap-2">
|
||||
<span>{{ employee.site?.name ?? 'Sans site' }}</span>
|
||||
<span>
|
||||
{{ employee.site?.name ?? 'Sans site' }}<span v-if="employee.currentContractNature"> — {{ contractNatureLabel(employee.currentContractNature) }}</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="isAdmin && (rows[employee.id]?.isSiteValid ?? false)"
|
||||
class="rounded-full bg-green-500 flex justify-center item-center text-white p-0.5"
|
||||
@@ -196,6 +198,7 @@
|
||||
import type {Employee} from '~/services/dto/employee'
|
||||
import TimeSelect from '~/components/ui/TimeSelect.vue'
|
||||
import type {HourRow} from './types'
|
||||
import { contractNatureLabel } from '~/utils/contract'
|
||||
|
||||
const rows = defineModel<Record<number, HourRow>>('rows', {required: true})
|
||||
const bulkValidationInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
{{ row.firstName }} {{ row.lastName }}
|
||||
<span class="font-normal text-neutral-600">({{ row.contractName ?? '-' }})</span>
|
||||
</p>
|
||||
<p class="text-[11px] text-neutral-500 truncate">{{ row.siteName ?? 'Sans site' }}</p>
|
||||
<p class="text-[11px] text-neutral-500 truncate">
|
||||
{{ row.siteName ?? 'Sans site' }}<span v-if="row.contractNature"> — {{ contractNatureLabel(row.contractNature) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -81,6 +83,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { WeeklyWorkHourSummary } from '~/services/dto/work-hour'
|
||||
import { CONTRACT_TYPES, type ContractType } from '~/services/dto/contract'
|
||||
import { contractNatureLabel } from '~/utils/contract'
|
||||
|
||||
const isInterimContract = (contractType?: ContractType | null) => {
|
||||
return contractType === CONTRACT_TYPES.INTERIM
|
||||
|
||||
Reference in New Issue
Block a user