347 lines
14 KiB
Vue
347 lines
14 KiB
Vue
<template>
|
|
<section class="mt-8">
|
|
<div class="overflow-hidden bg-white">
|
|
<div
|
|
class="grid grid-cols-6 border border-black bg-tertiary-500 px-6 py-3 text-[20px] font-semibold text-black rounded-t-md">
|
|
<p>Mois</p>
|
|
<p>Nombre de Km</p>
|
|
<p>Montant €</p>
|
|
<p>Commentaire</p>
|
|
<p>Justif. Km</p>
|
|
<p>Justif. Montant</p>
|
|
</div>
|
|
<div v-if="allowances.length === 0" class="px-6 py-4 text-[20px] font-bold text-primary-500 border-x border-b border-primary-500 rounded-b-md">
|
|
Aucun frais kilométrique.
|
|
</div>
|
|
<div v-else class="border-x border-b border-primary-500 rounded-b-md">
|
|
<div
|
|
v-for="item in allowances"
|
|
:key="item.id"
|
|
class="grid grid-cols-6 border-b border-primary-500 px-6 py-3 text-md font-bold text-primary-500 last:border-b-0 cursor-pointer hover:bg-tertiary-500"
|
|
@click="onOpenEditDrawer(item)"
|
|
>
|
|
<p>{{ formatMonth(item.month) }}</p>
|
|
<p>{{ item.kilometers }}</p>
|
|
<p>{{ item.amount ? item.amount + ' €' : '-' }}</p>
|
|
<p>{{ item.comment ?? '-' }}</p>
|
|
<p class="min-w-0">
|
|
<a
|
|
v-if="item.receiptPath"
|
|
:href="getKmReceiptUrl(props.apiBase, item.id)"
|
|
target="_blank"
|
|
class="text-primary-500 hover:text-secondary-500 flex gap-2 items-center"
|
|
@click.stop
|
|
>
|
|
<Icon name="mdi:file-download-outline" size="20" class="shrink-0"/>
|
|
<span class="truncate">{{ item.receiptName ?? 'Télécharger' }}</span>
|
|
</a>
|
|
<span v-else>-</span>
|
|
</p>
|
|
<p class="min-w-0">
|
|
<a
|
|
v-if="item.amountReceiptPath"
|
|
:href="getAmountReceiptUrl(props.apiBase, item.id)"
|
|
target="_blank"
|
|
class="text-primary-500 hover:text-secondary-500 flex gap-2 items-center"
|
|
@click.stop
|
|
>
|
|
<Icon name="mdi:file-download-outline" size="20" class="shrink-0"/>
|
|
<span class="truncate">{{ item.amountReceiptName ?? 'Télécharger' }}</span>
|
|
</a>
|
|
<span v-else>-</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-center mb-4 mt-8">
|
|
<button
|
|
type="button"
|
|
class="flex w-[200px] items-center justify-center gap-2 rounded-md bg-primary-500 px-4 py-2 text-md text-white disabled:cursor-not-allowed disabled:opacity-50"
|
|
@click="onOpenCreateDrawer"
|
|
>
|
|
+ Ajouter
|
|
</button>
|
|
</div>
|
|
|
|
|
|
<AppDrawer v-model="isDrawerOpen" title="Frais">
|
|
<form class="space-y-4" @submit.prevent="onSubmit">
|
|
<div>
|
|
<label class="text-md font-semibold text-neutral-700" for="mileage-month">
|
|
Mois <span class="text-red-600">*</span>
|
|
</label>
|
|
<input
|
|
id="mileage-month"
|
|
v-model="form.month"
|
|
type="month"
|
|
class="capitalize 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>
|
|
<label class="text-md font-semibold text-neutral-700" for="mileage-kilometers">
|
|
Nombre de Km
|
|
</label>
|
|
<input
|
|
id="mileage-kilometers"
|
|
v-model.number="form.kilometers"
|
|
type="number"
|
|
step="0.1"
|
|
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>
|
|
<label class="text-md font-semibold text-neutral-700" for="mileage-amount">
|
|
Montant (€)
|
|
</label>
|
|
<input
|
|
id="mileage-amount"
|
|
v-model.number="form.amount"
|
|
type="number"
|
|
step="0.01"
|
|
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"
|
|
/>
|
|
<p class="mt-1 text-sm text-neutral-500">Au moins un des deux champs doit être rempli</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="text-md font-semibold text-neutral-700" for="mileage-km-receipt">
|
|
Justificatif Km
|
|
</label>
|
|
<div v-if="isEditing && editingItem?.receiptName" class="mt-1 text-sm text-neutral-500">
|
|
Fichier actuel : {{ editingItem.receiptName }}
|
|
</div>
|
|
<input
|
|
id="mileage-km-receipt"
|
|
ref="kmFileInput"
|
|
type="file"
|
|
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 file:mr-3 file:rounded file:border-0 file:bg-primary-500 file:px-3 file:py-1 file:text-sm file:text-white"
|
|
@change="onKmFileChange"
|
|
/>
|
|
<p v-if="kmFileError" class="mt-1 text-sm text-red-600">{{ kmFileError }}</p>
|
|
<p v-else class="mt-1 text-sm text-neutral-500">Fichier au format pdf</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="text-md font-semibold text-neutral-700" for="mileage-amount-receipt">
|
|
Justificatif Montant
|
|
</label>
|
|
<div v-if="isEditing && editingItem?.amountReceiptName" class="mt-1 text-sm text-neutral-500">
|
|
Fichier actuel : {{ editingItem.amountReceiptName }}
|
|
</div>
|
|
<input
|
|
id="mileage-amount-receipt"
|
|
ref="amountFileInput"
|
|
type="file"
|
|
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 file:mr-3 file:rounded file:border-0 file:bg-primary-500 file:px-3 file:py-1 file:text-sm file:text-white"
|
|
@change="onAmountFileChange"
|
|
/>
|
|
<p v-if="amountFileError" class="mt-1 text-sm text-red-600">{{ amountFileError }}</p>
|
|
<p v-else class="mt-1 text-sm text-neutral-500">Fichier au format pdf</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="text-md font-semibold text-neutral-700" for="mileage-comment">
|
|
Commentaire
|
|
</label>
|
|
<textarea
|
|
id="mileage-comment"
|
|
v-model="form.comment"
|
|
rows="3"
|
|
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"
|
|
placeholder="Commentaire..."
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="isEditing" class="grid grid-cols-2 gap-3 pt-2">
|
|
<button
|
|
type="button"
|
|
class="flex items-center justify-center rounded-md bg-red-500 px-4 py-2 text-md font-semibold text-white hover:bg-red-600"
|
|
@click="onDelete"
|
|
>
|
|
Supprimer
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
class="flex items-center justify-center rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
:disabled="!isFormValid"
|
|
>
|
|
Modifier
|
|
</button>
|
|
</div>
|
|
<div v-else class="flex justify-center pt-2">
|
|
<button
|
|
type="submit"
|
|
class="flex w-[200px] items-center justify-center gap-2 rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
:disabled="!isFormValid"
|
|
>
|
|
+ Ajouter
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</AppDrawer>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type {MileageAllowance} from '~/services/dto/mileage-allowance'
|
|
import {getKmReceiptUrl, getAmountReceiptUrl} from '~/services/mileage-allowances'
|
|
import AppDrawer from '~/components/AppDrawer.vue'
|
|
|
|
const props = defineProps<{
|
|
allowances: MileageAllowance[]
|
|
apiBase: string
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(event: 'create', data: { month: string; kilometers: number; amount: number; comment?: string }, kmFile?: File, amountFile?: File): void
|
|
(event: 'update', id: number, data: { month: string; kilometers: number; amount: number; comment?: string }, kmFile?: File, amountFile?: File): void
|
|
(event: 'delete', id: number): void
|
|
}>()
|
|
|
|
const isDrawerOpen = ref(false)
|
|
const isEditing = ref(false)
|
|
const editingItem = ref<MileageAllowance | null>(null)
|
|
const selectedKmFile = ref<File | undefined>(undefined)
|
|
const selectedAmountFile = ref<File | undefined>(undefined)
|
|
const kmFileInput = ref<HTMLInputElement | null>(null)
|
|
const amountFileInput = ref<HTMLInputElement | null>(null)
|
|
const kmFileError = ref('')
|
|
const amountFileError = ref('')
|
|
|
|
const currentYearMonth = () => {
|
|
const now = new Date()
|
|
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
|
|
}
|
|
|
|
const form = reactive({
|
|
month: currentYearMonth(),
|
|
kilometers: 0,
|
|
amount: 0,
|
|
comment: ''
|
|
})
|
|
|
|
const isFormValid = computed(() => {
|
|
return form.month && (form.kilometers > 0 || form.amount > 0) && !kmFileError.value && !amountFileError.value
|
|
})
|
|
|
|
const monthLabels: Record<number, string> = {
|
|
1: 'Janvier',
|
|
2: 'Février',
|
|
3: 'Mars',
|
|
4: 'Avril',
|
|
5: 'Mai',
|
|
6: 'Juin',
|
|
7: 'Juillet',
|
|
8: 'Août',
|
|
9: 'Septembre',
|
|
10: 'Octobre',
|
|
11: 'Novembre',
|
|
12: 'Décembre'
|
|
}
|
|
|
|
const formatMonth = (dateStr: string): string => {
|
|
const date = new Date(dateStr)
|
|
if (Number.isNaN(date.getTime())) return dateStr
|
|
const month = date.getMonth() + 1
|
|
const year = date.getFullYear()
|
|
return `${monthLabels[month]} ${year}`
|
|
}
|
|
|
|
const resetForm = () => {
|
|
form.month = currentYearMonth()
|
|
form.kilometers = 0
|
|
form.amount = 0
|
|
form.comment = ''
|
|
selectedKmFile.value = undefined
|
|
selectedAmountFile.value = undefined
|
|
kmFileError.value = ''
|
|
amountFileError.value = ''
|
|
if (kmFileInput.value) {
|
|
kmFileInput.value.value = ''
|
|
}
|
|
if (amountFileInput.value) {
|
|
amountFileInput.value.value = ''
|
|
}
|
|
}
|
|
|
|
const onOpenCreateDrawer = () => {
|
|
isEditing.value = false
|
|
editingItem.value = null
|
|
resetForm()
|
|
isDrawerOpen.value = true
|
|
}
|
|
|
|
const onOpenEditDrawer = (item: MileageAllowance) => {
|
|
isEditing.value = true
|
|
editingItem.value = item
|
|
// Extract YYYY-MM from YYYY-MM-DD
|
|
form.month = item.month.substring(0, 7)
|
|
form.kilometers = item.kilometers
|
|
form.amount = item.amount
|
|
form.comment = item.comment ?? ''
|
|
selectedKmFile.value = undefined
|
|
selectedAmountFile.value = undefined
|
|
if (kmFileInput.value) {
|
|
kmFileInput.value.value = ''
|
|
}
|
|
if (amountFileInput.value) {
|
|
amountFileInput.value.value = ''
|
|
}
|
|
isDrawerOpen.value = true
|
|
}
|
|
|
|
const onKmFileChange = (event: Event) => {
|
|
const target = event.target as HTMLInputElement
|
|
const file = target.files?.[0]
|
|
if (file && file.type !== 'application/pdf') {
|
|
kmFileError.value = 'Seuls les fichiers PDF sont acceptés.'
|
|
selectedKmFile.value = undefined
|
|
target.value = ''
|
|
return
|
|
}
|
|
kmFileError.value = ''
|
|
selectedKmFile.value = file ?? undefined
|
|
}
|
|
|
|
const onAmountFileChange = (event: Event) => {
|
|
const target = event.target as HTMLInputElement
|
|
const file = target.files?.[0]
|
|
if (file && file.type !== 'application/pdf') {
|
|
amountFileError.value = 'Seuls les fichiers PDF sont acceptés.'
|
|
selectedAmountFile.value = undefined
|
|
target.value = ''
|
|
return
|
|
}
|
|
amountFileError.value = ''
|
|
selectedAmountFile.value = file ?? undefined
|
|
}
|
|
|
|
const onSubmit = () => {
|
|
const data = {
|
|
month: `${form.month}-01`,
|
|
kilometers: form.kilometers,
|
|
amount: form.amount,
|
|
comment: form.comment || undefined
|
|
}
|
|
|
|
if (isEditing.value && editingItem.value) {
|
|
emit('update', editingItem.value.id, data, selectedKmFile.value, selectedAmountFile.value)
|
|
} else {
|
|
emit('create', data, selectedKmFile.value, selectedAmountFile.value)
|
|
}
|
|
isDrawerOpen.value = false
|
|
}
|
|
|
|
const onDelete = () => {
|
|
if (!editingItem.value) return
|
|
const ok = window.confirm('Supprimer ce frais kilométrique ?')
|
|
if (!ok) return
|
|
emit('delete', editingItem.value.id)
|
|
isDrawerOpen.value = false
|
|
}
|
|
</script>
|