114 lines
3.8 KiB
Vue
114 lines
3.8 KiB
Vue
<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>
|