diff --git a/frontend/components/employees/BonusTab.vue b/frontend/components/employees/BonusTab.vue new file mode 100644 index 0000000..6324eea --- /dev/null +++ b/frontend/components/employees/BonusTab.vue @@ -0,0 +1,207 @@ + + + + + Mois + Montant € + Commentaire + + + Aucune prime. + + + + {{ formatMonth(item.month) }} + {{ item.amount }} € + {{ item.comment ?? '-' }} + + + + + + + Ajouter + + + + + + + + Mois * + + + + + + + Montant (€) * + + + + + + + Commentaire + + + + + + + Supprimer + + + Modifier + + + + + + Ajouter + + + + + + + + diff --git a/frontend/composables/useEmployeeBonus.ts b/frontend/composables/useEmployeeBonus.ts new file mode 100644 index 0000000..8f331e9 --- /dev/null +++ b/frontend/composables/useEmployeeBonus.ts @@ -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, reloadEmployee: () => Promise) => { + const bonuses = ref([]) + 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 + } +} diff --git a/frontend/composables/useEmployeeDetailPage.ts b/frontend/composables/useEmployeeDetailPage.ts index 73a399a..df00a84 100644 --- a/frontend/composables/useEmployeeDetailPage.ts +++ b/frontend/composables/useEmployeeDetailPage.ts @@ -6,7 +6,7 @@ export const useEmployeeDetailPage = () => { const route = useRoute() const employee = ref(null) 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 showRttTab = computed(() => employee.value?.contract?.type !== CONTRACT_TYPES.FORFAIT) @@ -39,6 +39,7 @@ export const useEmployeeDetailPage = () => { leave.resetLoaded() rtt.resetLoaded() mileage.resetLoaded() + bonus.resetLoaded() if (activeTab.value === 'leave' && showLeaveTab.value) { await leave.loadLeaveData() @@ -46,6 +47,8 @@ export const useEmployeeDetailPage = () => { await rtt.loadRttData() } else if (activeTab.value === 'mileage') { await mileage.loadMileageData() + } else if (activeTab.value === 'bonus') { + await bonus.loadBonusData() } } finally { isLoading.value = false @@ -56,6 +59,7 @@ export const useEmployeeDetailPage = () => { const leave = useEmployeeLeave(employee, loadEmployee) const rtt = useEmployeeRtt(employee, loadEmployee) const mileage = useEmployeeMileage(employee, loadEmployee) + const bonus = useEmployeeBonus(employee, loadEmployee) watch(activeTab, (tab) => { if (tab === 'leave' && !leave.leaveDataLoaded.value && showLeaveTab.value) { @@ -64,6 +68,8 @@ export const useEmployeeDetailPage = () => { rtt.loadRttData() } else if (tab === 'mileage' && !mileage.mileageDataLoaded.value) { mileage.loadMileageData() + } else if (tab === 'bonus' && !bonus.bonusDataLoaded.value) { + bonus.loadBonusData() } }) @@ -82,6 +88,7 @@ export const useEmployeeDetailPage = () => { ...contract, ...leave, ...rtt, - ...mileage + ...mileage, + ...bonus } } diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index 987b1d8..1d7a261 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -41,6 +41,11 @@ "create": "Impossible de créer le frais kilométrique.", "update": "Impossible de mettre à jour 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": { @@ -77,6 +82,11 @@ "create": "Frais kilométrique créé.", "update": "Frais kilométrique mis à jour.", "delete": "Frais kilométrique supprimé." + }, + "bonus": { + "create": "Prime créée.", + "update": "Prime mise à jour.", + "delete": "Prime supprimée." } } } diff --git a/frontend/pages/employees/[id].vue b/frontend/pages/employees/[id].vue index 904f464..0e63a9e 100644 --- a/frontend/pages/employees/[id].vue +++ b/frontend/pages/employees/[id].vue @@ -65,6 +65,16 @@ Frais Kms + + + Prime + @@ -141,6 +151,19 @@ @delete="submitDeleteMileage" /> + + + Chargement... + + + @@ -203,7 +226,12 @@ const { mileageApiBase, submitCreateMileage, submitUpdateMileage, - submitDeleteMileage + submitDeleteMileage, + bonuses, + isBonusLoading, + submitCreateBonus, + submitUpdateBonus, + submitDeleteBonus } = useEmployeeDetailPage() useHead(() => ({ diff --git a/frontend/services/bonuses.ts b/frontend/services/bonuses.ts new file mode 100644 index 0000000..9d7d1cb --- /dev/null +++ b/frontend/services/bonuses.ts @@ -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( + '/bonuses', + { employee: `/api/employees/${employeeId}` }, + { toast: false } + ) + return extractItems(data) +} + +export const createBonus = async (data: { + employeeId: number + month: string + amount: number + comment?: string +}) => { + const api = useApi() + return api.post('/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(`/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' + }) +} diff --git a/frontend/services/dto/bonus.ts b/frontend/services/dto/bonus.ts new file mode 100644 index 0000000..95071a1 --- /dev/null +++ b/frontend/services/dto/bonus.ts @@ -0,0 +1,7 @@ +export type Bonus = { + id: number + month: string + amount: number + comment: string | null + createdAt: string +} diff --git a/migrations/Version20260313151220.php b/migrations/Version20260313151220.php new file mode 100644 index 0000000..c230b1c --- /dev/null +++ b/migrations/Version20260313151220.php @@ -0,0 +1,31 @@ +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'); + } +} diff --git a/src/Entity/Bonus.php b/src/Entity/Bonus.php new file mode 100644 index 0000000..c1e3186 --- /dev/null +++ b/src/Entity/Bonus.php @@ -0,0 +1,145 @@ + ['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; + } +} diff --git a/src/Repository/BonusRepository.php b/src/Repository/BonusRepository.php new file mode 100644 index 0000000..21e93ac --- /dev/null +++ b/src/Repository/BonusRepository.php @@ -0,0 +1,20 @@ + + */ +final class BonusRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Bonus::class); + } +}
Mois
Montant €
Commentaire
{{ formatMonth(item.month) }}
{{ item.amount }} €
{{ item.comment ?? '-' }}