Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43957903b0 | ||
| d455bb77a3 |
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.1.37'
|
app.version: '0.1.38'
|
||||||
|
|||||||
207
frontend/components/employees/BonusTab.vue
Normal file
207
frontend/components/employees/BonusTab.vue
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
<template>
|
||||||
|
<section class="mt-8">
|
||||||
|
<div class="overflow-hidden bg-white">
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-3 border border-black bg-tertiary-500 px-6 py-3 text-[20px] font-semibold text-black rounded-t-md">
|
||||||
|
<p>Mois</p>
|
||||||
|
<p>Montant €</p>
|
||||||
|
<p>Commentaire</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="bonuses.length === 0" class="px-6 py-4 text-[20px] font-bold text-primary-500 border-x border-b border-primary-500 rounded-b-md">
|
||||||
|
Aucune prime.
|
||||||
|
</div>
|
||||||
|
<div v-else class="border-x border-b border-primary-500 rounded-b-md">
|
||||||
|
<div
|
||||||
|
v-for="item in bonuses"
|
||||||
|
:key="item.id"
|
||||||
|
class="grid grid-cols-3 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.amount }} €</p>
|
||||||
|
<p>{{ item.comment ?? '-' }}</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="isEditing ? 'Modification prime' : 'Nouvelle prime'">
|
||||||
|
<form class="space-y-4" @submit.prevent="onSubmit">
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="bonus-month">
|
||||||
|
Mois <span class="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="bonus-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="bonus-amount">
|
||||||
|
Montant (€) <span class="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="bonus-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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="bonus-comment">
|
||||||
|
Commentaire
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="bonus-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 { Bonus } from '~/services/dto/bonus'
|
||||||
|
import AppDrawer from '~/components/AppDrawer.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
bonuses: Bonus[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'create', data: { month: string; amount: number; comment?: string }): void
|
||||||
|
(event: 'update', id: number, data: { month: string; amount: number; comment?: string }): void
|
||||||
|
(event: 'delete', id: number): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isDrawerOpen = ref(false)
|
||||||
|
const isEditing = ref(false)
|
||||||
|
const editingItem = ref<Bonus | null>(null)
|
||||||
|
|
||||||
|
const currentYearMonth = () => {
|
||||||
|
const now = new Date()
|
||||||
|
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
month: currentYearMonth(),
|
||||||
|
amount: 0,
|
||||||
|
comment: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const isFormValid = computed(() => {
|
||||||
|
return form.month && form.amount > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
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.amount = 0
|
||||||
|
form.comment = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const onOpenCreateDrawer = () => {
|
||||||
|
isEditing.value = false
|
||||||
|
editingItem.value = null
|
||||||
|
resetForm()
|
||||||
|
isDrawerOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onOpenEditDrawer = (item: Bonus) => {
|
||||||
|
isEditing.value = true
|
||||||
|
editingItem.value = item
|
||||||
|
form.month = item.month.substring(0, 7)
|
||||||
|
form.amount = item.amount
|
||||||
|
form.comment = item.comment ?? ''
|
||||||
|
isDrawerOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
const data = {
|
||||||
|
month: `${form.month}-01`,
|
||||||
|
amount: form.amount,
|
||||||
|
comment: form.comment || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditing.value && editingItem.value) {
|
||||||
|
emit('update', editingItem.value.id, data)
|
||||||
|
} else {
|
||||||
|
emit('create', data)
|
||||||
|
}
|
||||||
|
isDrawerOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDelete = () => {
|
||||||
|
if (!editingItem.value) return
|
||||||
|
const ok = window.confirm('Supprimer cette prime ?')
|
||||||
|
if (!ok) return
|
||||||
|
emit('delete', editingItem.value.id)
|
||||||
|
isDrawerOpen.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
62
frontend/composables/useEmployeeBonus.ts
Normal file
62
frontend/composables/useEmployeeBonus.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
import type { Bonus } from '~/services/dto/bonus'
|
||||||
|
import type { Employee } from '~/services/dto/employee'
|
||||||
|
import {
|
||||||
|
listBonuses,
|
||||||
|
createBonus,
|
||||||
|
updateBonus,
|
||||||
|
deleteBonus
|
||||||
|
} from '~/services/bonuses'
|
||||||
|
|
||||||
|
export const useEmployeeBonus = (employee: Ref<Employee | null>, reloadEmployee: () => Promise<void>) => {
|
||||||
|
const bonuses = ref<Bonus[]>([])
|
||||||
|
const isBonusLoading = ref(false)
|
||||||
|
const bonusDataLoaded = ref(false)
|
||||||
|
|
||||||
|
const loadBonusData = async () => {
|
||||||
|
if (!employee.value || isBonusLoading.value) return
|
||||||
|
isBonusLoading.value = true
|
||||||
|
try {
|
||||||
|
bonuses.value = await listBonuses(employee.value.id)
|
||||||
|
bonusDataLoaded.value = true
|
||||||
|
} finally {
|
||||||
|
isBonusLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetLoaded = () => {
|
||||||
|
bonusDataLoaded.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitCreateBonus = async (data: { month: string; amount: number; comment?: string }) => {
|
||||||
|
if (!employee.value) return
|
||||||
|
await createBonus({
|
||||||
|
employeeId: employee.value.id,
|
||||||
|
month: data.month,
|
||||||
|
amount: data.amount,
|
||||||
|
comment: data.comment
|
||||||
|
})
|
||||||
|
await reloadEmployee()
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitUpdateBonus = async (id: number, data: { month: string; amount: number; comment?: string }) => {
|
||||||
|
await updateBonus(id, data)
|
||||||
|
await reloadEmployee()
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitDeleteBonus = async (id: number) => {
|
||||||
|
await deleteBonus(id)
|
||||||
|
await reloadEmployee()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bonuses,
|
||||||
|
isBonusLoading,
|
||||||
|
bonusDataLoaded,
|
||||||
|
loadBonusData,
|
||||||
|
resetLoaded,
|
||||||
|
submitCreateBonus,
|
||||||
|
submitUpdateBonus,
|
||||||
|
submitDeleteBonus
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ export const useEmployeeDetailPage = () => {
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const employee = ref<Employee | null>(null)
|
const employee = ref<Employee | null>(null)
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const activeTab = ref<'contract' | 'leave' | 'rtt' | 'mileage'>('contract')
|
const activeTab = ref<'contract' | 'leave' | 'rtt' | 'mileage' | 'bonus'>('contract')
|
||||||
|
|
||||||
const showLeaveTab = computed(() => employee.value?.currentContractNature !== 'INTERIM')
|
const showLeaveTab = computed(() => employee.value?.currentContractNature !== 'INTERIM')
|
||||||
const showRttTab = computed(() => employee.value?.contract?.type !== CONTRACT_TYPES.FORFAIT)
|
const showRttTab = computed(() => employee.value?.contract?.type !== CONTRACT_TYPES.FORFAIT)
|
||||||
@@ -39,6 +39,7 @@ export const useEmployeeDetailPage = () => {
|
|||||||
leave.resetLoaded()
|
leave.resetLoaded()
|
||||||
rtt.resetLoaded()
|
rtt.resetLoaded()
|
||||||
mileage.resetLoaded()
|
mileage.resetLoaded()
|
||||||
|
bonus.resetLoaded()
|
||||||
|
|
||||||
if (activeTab.value === 'leave' && showLeaveTab.value) {
|
if (activeTab.value === 'leave' && showLeaveTab.value) {
|
||||||
await leave.loadLeaveData()
|
await leave.loadLeaveData()
|
||||||
@@ -46,6 +47,8 @@ export const useEmployeeDetailPage = () => {
|
|||||||
await rtt.loadRttData()
|
await rtt.loadRttData()
|
||||||
} else if (activeTab.value === 'mileage') {
|
} else if (activeTab.value === 'mileage') {
|
||||||
await mileage.loadMileageData()
|
await mileage.loadMileageData()
|
||||||
|
} else if (activeTab.value === 'bonus') {
|
||||||
|
await bonus.loadBonusData()
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
@@ -56,6 +59,7 @@ export const useEmployeeDetailPage = () => {
|
|||||||
const leave = useEmployeeLeave(employee, loadEmployee)
|
const leave = useEmployeeLeave(employee, loadEmployee)
|
||||||
const rtt = useEmployeeRtt(employee, loadEmployee)
|
const rtt = useEmployeeRtt(employee, loadEmployee)
|
||||||
const mileage = useEmployeeMileage(employee, loadEmployee)
|
const mileage = useEmployeeMileage(employee, loadEmployee)
|
||||||
|
const bonus = useEmployeeBonus(employee, loadEmployee)
|
||||||
|
|
||||||
watch(activeTab, (tab) => {
|
watch(activeTab, (tab) => {
|
||||||
if (tab === 'leave' && !leave.leaveDataLoaded.value && showLeaveTab.value) {
|
if (tab === 'leave' && !leave.leaveDataLoaded.value && showLeaveTab.value) {
|
||||||
@@ -64,6 +68,8 @@ export const useEmployeeDetailPage = () => {
|
|||||||
rtt.loadRttData()
|
rtt.loadRttData()
|
||||||
} else if (tab === 'mileage' && !mileage.mileageDataLoaded.value) {
|
} else if (tab === 'mileage' && !mileage.mileageDataLoaded.value) {
|
||||||
mileage.loadMileageData()
|
mileage.loadMileageData()
|
||||||
|
} else if (tab === 'bonus' && !bonus.bonusDataLoaded.value) {
|
||||||
|
bonus.loadBonusData()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -82,6 +88,7 @@ export const useEmployeeDetailPage = () => {
|
|||||||
...contract,
|
...contract,
|
||||||
...leave,
|
...leave,
|
||||||
...rtt,
|
...rtt,
|
||||||
...mileage
|
...mileage,
|
||||||
|
...bonus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,11 @@
|
|||||||
"create": "Impossible de créer le frais kilométrique.",
|
"create": "Impossible de créer le frais kilométrique.",
|
||||||
"update": "Impossible de mettre à jour le frais kilométrique.",
|
"update": "Impossible de mettre à jour le frais kilométrique.",
|
||||||
"delete": "Impossible de supprimer le frais kilométrique."
|
"delete": "Impossible de supprimer le frais kilométrique."
|
||||||
|
},
|
||||||
|
"bonus": {
|
||||||
|
"create": "Impossible de créer la prime.",
|
||||||
|
"update": "Impossible de mettre à jour la prime.",
|
||||||
|
"delete": "Impossible de supprimer la prime."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
@@ -77,6 +82,11 @@
|
|||||||
"create": "Frais kilométrique créé.",
|
"create": "Frais kilométrique créé.",
|
||||||
"update": "Frais kilométrique mis à jour.",
|
"update": "Frais kilométrique mis à jour.",
|
||||||
"delete": "Frais kilométrique supprimé."
|
"delete": "Frais kilométrique supprimé."
|
||||||
|
},
|
||||||
|
"bonus": {
|
||||||
|
"create": "Prime créée.",
|
||||||
|
"update": "Prime mise à jour.",
|
||||||
|
"delete": "Prime supprimée."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,16 @@
|
|||||||
<Icon name="mdi:car-outline" size="24" class="align-self"/>
|
<Icon name="mdi:car-outline" size="24" class="align-self"/>
|
||||||
Frais Kms
|
Frais Kms
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="pb-2 border-b-2 flex items-center gap-3"
|
||||||
|
:class="activeTab === 'bonus'
|
||||||
|
? 'border-primary-500 text-primary-500'
|
||||||
|
: 'border-transparent text-primary-500/50 hover:text-primary-500'"
|
||||||
|
@click="activeTab = 'bonus'"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:money-100" size="24" class="align-self"/>
|
||||||
|
Prime
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="min-h-0 flex-1">
|
<div class="min-h-0 flex-1">
|
||||||
@@ -141,6 +151,19 @@
|
|||||||
@delete="submitDeleteMileage"
|
@delete="submitDeleteMileage"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="activeTab === 'bonus'" class="h-full">
|
||||||
|
<div v-if="isBonusLoading" class="mt-6 rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
|
||||||
|
Chargement...
|
||||||
|
</div>
|
||||||
|
<EmployeesBonusTab
|
||||||
|
v-else
|
||||||
|
class="h-full"
|
||||||
|
:bonuses="bonuses"
|
||||||
|
@create="submitCreateBonus"
|
||||||
|
@update="submitUpdateBonus"
|
||||||
|
@delete="submitDeleteBonus"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,7 +226,12 @@ const {
|
|||||||
mileageApiBase,
|
mileageApiBase,
|
||||||
submitCreateMileage,
|
submitCreateMileage,
|
||||||
submitUpdateMileage,
|
submitUpdateMileage,
|
||||||
submitDeleteMileage
|
submitDeleteMileage,
|
||||||
|
bonuses,
|
||||||
|
isBonusLoading,
|
||||||
|
submitCreateBonus,
|
||||||
|
submitUpdateBonus,
|
||||||
|
submitDeleteBonus
|
||||||
} = useEmployeeDetailPage()
|
} = useEmployeeDetailPage()
|
||||||
|
|
||||||
useHead(() => ({
|
useHead(() => ({
|
||||||
|
|||||||
54
frontend/services/bonuses.ts
Normal file
54
frontend/services/bonuses.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { Bonus } from './dto/bonus'
|
||||||
|
import { extractItems } from '~/utils/api'
|
||||||
|
|
||||||
|
export const listBonuses = async (employeeId: number) => {
|
||||||
|
const api = useApi()
|
||||||
|
const data = await api.get<Bonus[] | { 'hydra:member'?: Bonus[] }>(
|
||||||
|
'/bonuses',
|
||||||
|
{ employee: `/api/employees/${employeeId}` },
|
||||||
|
{ toast: false }
|
||||||
|
)
|
||||||
|
return extractItems<Bonus>(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createBonus = async (data: {
|
||||||
|
employeeId: number
|
||||||
|
month: string
|
||||||
|
amount: number
|
||||||
|
comment?: string
|
||||||
|
}) => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post<Bonus>('/bonuses', {
|
||||||
|
employee: `/api/employees/${data.employeeId}`,
|
||||||
|
month: data.month,
|
||||||
|
amount: data.amount,
|
||||||
|
comment: data.comment
|
||||||
|
}, {
|
||||||
|
toastSuccessKey: 'success.bonus.create',
|
||||||
|
toastErrorKey: 'errors.bonus.create'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateBonus = async (id: number, data: {
|
||||||
|
month: string
|
||||||
|
amount: number
|
||||||
|
comment?: string
|
||||||
|
}) => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.patch<Bonus>(`/bonuses/${id}`, {
|
||||||
|
month: data.month,
|
||||||
|
amount: data.amount,
|
||||||
|
comment: data.comment
|
||||||
|
}, {
|
||||||
|
toastSuccessKey: 'success.bonus.update',
|
||||||
|
toastErrorKey: 'errors.bonus.update'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteBonus = async (id: number) => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.delete(`/bonuses/${id}`, {}, {
|
||||||
|
toastSuccessKey: 'success.bonus.delete',
|
||||||
|
toastErrorKey: 'errors.bonus.delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
7
frontend/services/dto/bonus.ts
Normal file
7
frontend/services/dto/bonus.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export type Bonus = {
|
||||||
|
id: number
|
||||||
|
month: string
|
||||||
|
amount: number
|
||||||
|
comment: string | null
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
31
migrations/Version20260313151220.php
Normal file
31
migrations/Version20260313151220.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260313151220 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create bonuses table';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE TABLE bonuses (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, employee_id INT NOT NULL, month DATE NOT NULL, amount DOUBLE PRECISION NOT NULL, comment TEXT DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_8535CFD28C03F15C ON bonuses (employee_id)');
|
||||||
|
$this->addSql('COMMENT ON COLUMN bonuses.created_at IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN bonuses.month IS \'(DC2Type:date_immutable)\'');
|
||||||
|
$this->addSql('ALTER TABLE bonuses ADD CONSTRAINT FK_8535CFD28C03F15C FOREIGN KEY (employee_id) REFERENCES employees (id) NOT DEFERRABLE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE bonuses DROP CONSTRAINT FK_8535CFD28C03F15C');
|
||||||
|
$this->addSql('DROP TABLE bonuses');
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/Entity/Bonus.php
Normal file
145
src/Entity/Bonus.php
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||||
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use App\Repository\BonusRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(
|
||||||
|
security: "is_granted('ROLE_USER')"
|
||||||
|
),
|
||||||
|
new GetCollection(
|
||||||
|
security: "is_granted('ROLE_USER')"
|
||||||
|
),
|
||||||
|
new Post(
|
||||||
|
security: "is_granted('ROLE_ADMIN')"
|
||||||
|
),
|
||||||
|
new Patch(
|
||||||
|
security: "is_granted('ROLE_ADMIN')"
|
||||||
|
),
|
||||||
|
new Delete(
|
||||||
|
security: "is_granted('ROLE_ADMIN')"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
normalizationContext: [
|
||||||
|
'groups' => ['bonus:read', 'employee:read'],
|
||||||
|
'datetime_format' => 'Y-m-d',
|
||||||
|
],
|
||||||
|
denormalizationContext: [
|
||||||
|
'groups' => ['bonus:write'],
|
||||||
|
'datetime_format' => 'Y-m-d',
|
||||||
|
],
|
||||||
|
order: ['month' => 'DESC'],
|
||||||
|
paginationEnabled: false,
|
||||||
|
)]
|
||||||
|
#[ApiFilter(DateFilter::class, properties: ['month'])]
|
||||||
|
#[ApiFilter(SearchFilter::class, properties: ['employee' => 'exact'])]
|
||||||
|
#[ORM\Entity(repositoryClass: BonusRepository::class)]
|
||||||
|
#[ORM\Table(name: 'bonuses')]
|
||||||
|
class Bonus
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
#[Groups(['bonus:read'])]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Employee::class)]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
#[Groups(['bonus:read', 'bonus:write'])]
|
||||||
|
private ?Employee $employee = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'date_immutable')]
|
||||||
|
#[Groups(['bonus:read', 'bonus:write'])]
|
||||||
|
private ?DateTimeImmutable $month = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'float')]
|
||||||
|
#[Groups(['bonus:read', 'bonus:write'])]
|
||||||
|
private float $amount = 0;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'text', nullable: true)]
|
||||||
|
#[Groups(['bonus:read', 'bonus:write'])]
|
||||||
|
private ?string $comment = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime_immutable')]
|
||||||
|
#[Groups(['bonus:read'])]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->createdAt = new DateTimeImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmployee(): ?Employee
|
||||||
|
{
|
||||||
|
return $this->employee;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEmployee(?Employee $employee): self
|
||||||
|
{
|
||||||
|
$this->employee = $employee;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMonth(): ?DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->month;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMonth(?DateTimeImmutable $month): self
|
||||||
|
{
|
||||||
|
$this->month = $month;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAmount(): float
|
||||||
|
{
|
||||||
|
return $this->amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAmount(float $amount): self
|
||||||
|
{
|
||||||
|
$this->amount = $amount;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getComment(): ?string
|
||||||
|
{
|
||||||
|
return $this->comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setComment(?string $comment): self
|
||||||
|
{
|
||||||
|
$this->comment = $comment;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/Repository/BonusRepository.php
Normal file
20
src/Repository/BonusRepository.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\Bonus;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<Bonus>
|
||||||
|
*/
|
||||||
|
final class BonusRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, Bonus::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user