diff --git a/frontend/components/hours/HoursToolbar.vue b/frontend/components/hours/HoursToolbar.vue index 3a56390..fc31221 100644 --- a/frontend/components/hours/HoursToolbar.vue +++ b/frontend/components/hours/HoursToolbar.vue @@ -6,6 +6,7 @@ @@ -46,6 +47,7 @@ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3053476..a67afbf 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7,7 +7,7 @@ "name": "frontend", "hasInstallScript": true, "dependencies": { - "@malio/layer-ui": "^1.4.3", + "@malio/layer-ui": "^1.4.5", "@nuxt/icon": "^2.2.1", "@nuxtjs/i18n": "^10.2.1", "@pinia/nuxt": "^0.11.3", @@ -2222,9 +2222,9 @@ "license": "MIT" }, "node_modules/@malio/layer-ui": { - "version": "1.4.3", - "resolved": "https://gitea.malio.fr/api/packages/MALIO-DEV/npm/%40malio%2Flayer-ui/-/1.4.3/layer-ui-1.4.3.tgz", - "integrity": "sha512-XGR0VteuRGGizl8ZP2ZRlyWsdSTAwOYR7z5687Gx/SFr5eTg+poOV2NupqOuWCksxEcXA54vzzC0vMG8PbSvxg==", + "version": "1.4.5", + "resolved": "https://gitea.malio.fr/api/packages/MALIO-DEV/npm/%40malio%2Flayer-ui/-/1.4.5/layer-ui-1.4.5.tgz", + "integrity": "sha512-UfVkLJk3WWGoZE1eyei0pY45IUbZRzabJr2X6GNFabHd/8EmuXqwP+LxCl8wEAO4ODrNKsVLJdi0eL3Zekv4Dg==", "dependencies": { "@nuxt/icon": "^2.2.1", "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/frontend/package.json b/frontend/package.json index f49a379..ce7abf3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,7 @@ "dependencies": { "@nuxt/icon": "^2.2.1", "@nuxtjs/i18n": "^10.2.1", - "@malio/layer-ui": "^1.4.3", + "@malio/layer-ui": "^1.4.5", "@pinia/nuxt": "^0.11.3", "nuxt": "^4.3.0", "nuxt-toast": "^1.4.0", diff --git a/frontend/pages/absence-types.vue b/frontend/pages/absence-types.vue index 6523e24..c93ef41 100644 --- a/frontend/pages/absence-types.vue +++ b/frontend/pages/absence-types.vue @@ -2,13 +2,12 @@

Types de statut

- + />
- +
-
- - -

- Le code est obligatoire. -

-
-
- - -

- Le libellé est obligatoire. -

-
+ +
- - + +
@@ -130,32 +109,29 @@

- - + label="Modifier" + button-class="w-full" + :disabled="isSubmitting || !isFormValid" + />
- + label="Valider" + button-class="w-[200px]" + :disabled="isSubmitting || !isFormValid" + />
-
+
@@ -202,20 +178,6 @@ const showCodeError = computed(() => validationTouched.code && !isCodeValid.valu const showLabelError = computed(() => validationTouched.label && !isLabelValid.value) const showColorError = computed(() => validationTouched.color && !isColorValid.value) -const baseInputClass = - 'mt-2 w-full rounded-md border px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20' -const codeFieldClass = computed(() => { - if (showCodeError.value) { - return `${baseInputClass} border-red-500` - } - return `${baseInputClass} border-neutral-300` -}) -const labelFieldClass = computed(() => { - if (showLabelError.value) { - return `${baseInputClass} border-red-500` - } - return `${baseInputClass} border-neutral-300` -}) const colorFieldClass = computed(() => { const baseColorClass = 'h-10 w-16 cursor-pointer rounded-md border bg-white p-1' if (showColorError.value) { @@ -224,13 +186,6 @@ const colorFieldClass = computed(() => { return `${baseColorClass} border-neutral-300` }) -const submitButtonClass = computed(() => { - if (isSubmitting.value || !isFormValid.value) { - return 'opacity-50 cursor-not-allowed' - } - return '' -}) - const loadAbsenceTypes = async () => { isLoading.value = true try { diff --git a/frontend/pages/calendar.vue b/frontend/pages/calendar.vue index 33e25b4..fdf85e1 100644 --- a/frontend/pages/calendar.vue +++ b/frontend/pages/calendar.vue @@ -5,16 +5,13 @@
-
-
- -
-
+

Employés

- - - - + />
@@ -46,6 +31,7 @@ @@ -262,16 +248,58 @@ - + +
+ - +
+ + +
+ + + +
+ +
+
+
@@ -285,8 +313,6 @@ import {listContracts} from '~/services/contracts' import {createEmployee, deleteEmployee, listEmployees, updateEmployee} from '~/services/employees' import {listSites} from '~/services/sites' import {listInterimAgencies, type InterimAgency} from '~/services/interim-agencies' -import SalaryRecapDrawer from '~/components/SalaryRecapDrawer.vue' -import BulkYearlyHoursDrawer from '~/components/BulkYearlyHoursDrawer.vue' import {contractNatureLabel, isContractNature, requiresContractEndDate, showsContractEndDate} from '~/utils/contract' import {usePdfPrinter} from '~/composables/usePdfPrinter' @@ -297,9 +323,50 @@ useHead({ const isDrawerOpen = ref(false) const isSubmitting = ref(false) const isLoading = ref(false) -const isSalaryRecapOpen = ref(false) -const isYearlyHoursBulkOpen = ref(false) -const isYearlyHoursBulkLoading = ref(false) +const isExportDrawerOpen = ref(false) +const exportChoice = ref<'leave-recap' | 'salary-recap' | 'yearly-hours' | ''>('') +const exportYear = ref(new Date().getFullYear()) +const exportMonth = ref(new Date().getMonth() + 1) +const exportSalaryMonth = ref(`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`) + +const exportTypeOptions = [ + { label: 'Récap. congés', value: 'leave-recap' }, + { label: 'Récap. salaire', value: 'salary-recap' }, + { label: 'Heures annuelles', value: 'yearly-hours' } +] +const exportYearOptions = computed(() => { + const current = new Date().getFullYear() + return Array.from({ length: 6 }, (_, i) => ({ label: String(current - i), value: current - i })) +}) +const exportMonthOptions = [ + { label: 'Janvier', value: 1 }, + { label: 'Février', value: 2 }, + { label: 'Mars', value: 3 }, + { label: 'Avril', value: 4 }, + { label: 'Mai', value: 5 }, + { label: 'Juin', value: 6 }, + { label: 'Juillet', value: 7 }, + { label: 'Août', value: 8 }, + { label: 'Septembre', value: 9 }, + { label: 'Octobre', value: 10 }, + { label: 'Novembre', value: 11 }, + { label: 'Décembre', value: 12 } +] + +const isExportValid = computed(() => { + if (!exportChoice.value) return false + if (exportChoice.value === 'salary-recap') { + return exportSalaryMonth.value.trim() !== '' + } + if (exportChoice.value === 'yearly-hours') { + return exportYear.value > 0 && exportMonth.value !== '' + } + return true +}) + +const onExportChoiceChange = (value: string | number | null) => { + exportChoice.value = (value === null ? '' : String(value)) as 'leave-recap' | 'salary-recap' | 'yearly-hours' | '' +} const { printPdf } = usePdfPrinter() const sitesInitialized = ref(false) const editingEmployee = ref(null) @@ -632,26 +699,29 @@ const openCreate = () => { isDrawerOpen.value = true } -const handleLeaveRecapPrint = async () => { - await printPdf('/leave-recap/print') +const openExportDrawer = () => { + exportChoice.value = '' + const now = new Date() + exportYear.value = now.getFullYear() + exportMonth.value = now.getMonth() + 1 + exportSalaryMonth.value = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}` + isExportDrawerOpen.value = true } -const handleSalaryRecapPrint = async (month: string) => { - await printPdf(`/salary-recap/print?month=${month}`) - isSalaryRecapOpen.value = false -} - -const handleBulkYearlyHoursPrint = async (payload: { year: number; month: number | null }) => { - isYearlyHoursBulkLoading.value = true - try { - const monthParam = null !== payload.month ? `&month=${payload.month}` : '' - await printPdf(`/yearly-hours/print-all?year=${payload.year}${monthParam}`) - isYearlyHoursBulkOpen.value = false - } finally { - isYearlyHoursBulkLoading.value = false +const handleExportValidate = async () => { + if (!isExportValid.value) return + const choice = exportChoice.value + isExportDrawerOpen.value = false + if (choice === 'leave-recap') { + await printPdf('/leave-recap/print') + } else if (choice === 'salary-recap') { + await printPdf(`/salary-recap/print?month=${exportSalaryMonth.value}`) + } else if (choice === 'yearly-hours') { + await printPdf(`/yearly-hours/print-all?year=${exportYear.value}&month=${exportMonth.value}`) } } + const confirmDelete = async (employee: Employee) => { const ok = window.confirm(`Supprimer ${employee.firstName} ${employee.lastName} ?`) if (!ok) return diff --git a/frontend/pages/login.vue b/frontend/pages/login.vue index 95672b5..e5d1fd1 100644 --- a/frontend/pages/login.vue +++ b/frontend/pages/login.vue @@ -9,31 +9,18 @@ class="mt-8 space-y-6 rounded-lg border border-neutral-200 bg-white p-6 shadow-sm" @submit.prevent="handleSubmit" > -
- - -
+ -
- - -
+ + />
- +
-
- - -

- Le nom du site est obligatoire. -

-
+
- - + label="Modifier" + button-class="w-full" + :disabled="isSubmitting || !isFormValid" + />
- + label="Valider" + button-class="w-[200px]" + :disabled="isSubmitting || !isFormValid" + />
- + @@ -146,22 +134,6 @@ const isFormValid = computed(() => isNameValid.value) const showNameError = computed(() => validationTouched.name && !isNameValid.value) -const baseInputClass = - 'mt-2 w-full rounded-md border px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20' -const nameFieldClass = computed(() => { - if (showNameError.value) { - return `${baseInputClass} border-red-500` - } - return `${baseInputClass} border-neutral-300` -}) - -const submitButtonClass = computed(() => { - if (isSubmitting.value || !isFormValid.value) { - return 'opacity-50 cursor-not-allowed' - } - return '' -}) - const loadSites = async () => { isLoading.value = true try { diff --git a/frontend/pages/users.vue b/frontend/pages/users.vue index de052c4..79b3e34 100644 --- a/frontend/pages/users.vue +++ b/frontend/pages/users.vue @@ -2,13 +2,12 @@

Utilisateurs

- + />
-
-
- - -

- Le nom d'utilisateur est obligatoire. -

-
+
- - -

- Laisse vide pour ne pas changer le mot de passe. -

-

- Le mot de passe est obligatoire. -

@@ -172,40 +153,32 @@
- - -

- Sélectionne un employé. -

+

Sites autorisés

- +

Sélectionne au moins un site. @@ -213,44 +186,31 @@

- -

- Un compte verrouillé ne peut plus se connecter. -

+
- -

- Affiche l'onglet dans la sidebar et donne accès au tableau récap. -

+
- + :label="editingUser ? 'Modifier' : 'Valider'" + button-class="w-[200px]" + :disabled="isSubmitting || !isFormValid" + />
- + @@ -348,27 +308,13 @@ const getSiteLabels = (user: User) => { return names.length > 0 ? names.join(', ') : 'Sites sélectionnés' } -const baseInputClass = - 'mt-2 w-full rounded-md border px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20' -const usernameFieldClass = computed(() => { - if (showUsernameError.value) { - return `${baseInputClass} border-red-500` - } - return `${baseInputClass} border-neutral-300` -}) -const passwordFieldClass = computed(() => { - if (showPasswordError.value) { - return `${baseInputClass} border-red-500` - } - return `${baseInputClass} border-neutral-300` -}) +const employeeOptions = computed(() => + employees.value.map((e) => ({ label: `${e.firstName} ${e.lastName}`, value: e.id })) +) -const submitButtonClass = computed(() => { - if (isSubmitting.value || !isFormValid.value) { - return 'opacity-50 cursor-not-allowed' - } - return '' -}) +const onEmployeeChange = (value: string | number | null) => { + form.employeeId = value === null ? '' : Number(value) +} const loadData = async () => { isLoading.value = true