Browse Source

add the AttendanceBookingReason model, table and forms

Olivier Massot 1 year ago
parent
commit
7f6975e67c

+ 1 - 0
components/Layout/ParametersMenu.vue

@@ -110,6 +110,7 @@
   }
 
   :deep(.v-list-item .v-icon) {
+    max-width: 24px;
     margin-right: 10px;
   }
 

+ 13 - 1
components/Ui/Form.vue

@@ -161,6 +161,15 @@ const props = defineProps({
     type: String as PropType<'top' | 'bottom' | 'both'>,
     required: false,
     default: 'both'
+  },
+  /**
+   * Cette méthode sera exécutée avant la soumission des données à l'API
+   * Elle prends l'entité en paramètre, permet de la modifier et retourne l'entité qui en résulte.
+   */
+  preProcess: {
+    type: Function as PropType<(instance: ApiModel) => ApiModel>,
+    required: false,
+    default: (instance: ApiModel) => instance
   }
 })
 
@@ -218,8 +227,11 @@ const submit = async (next: string|null = null) => {
 
   try {
     usePageStore().loading = true
+
+    const entity = props.preProcess(props.entity)
+
     // TODO: est-ce qu'il faut re-fetch l'entité après le persist?
-    const updatedEntity = await em.persist(props.model, props.entity)
+    const updatedEntity = await em.persist(props.model, entity)
 
     if (props.refreshProfile) {
       await refreshProfile()

+ 10 - 0
components/Ui/Form/Creation.vue

@@ -3,6 +3,7 @@
       :model="model"
       :entity="entity"
       :submitActions="submitActions"
+      :preProcess="preProcess"
   >
     <template #form.button>
       <v-btn v-if="goBackRoute" class="theme-neutral mr-3" @click="quit">
@@ -54,6 +55,15 @@ const props = defineProps({
     type: Boolean,
     required: false,
     default: false
+  },
+  /**
+   * Cette méthode sera exécutée avant la soumission des données à l'API
+   * Elle prends l'entité en paramètre, permet de la modifier et retourne l'entité qui en résulte.
+   */
+  preProcess: {
+    type: Function as PropType<(instance: ApiModel) => ApiModel>,
+    required: false,
+    default: (instance: ApiModel) => instance
   }
 })
 

+ 1 - 1
components/Ui/Form/Edition.vue

@@ -62,7 +62,7 @@ const props = defineProps({
     default: false
   },
   /**
-   * Faut-il rafraichir le profil à la soumission du formulaire?
+   * Faut-il rafraichir le profil à la soumission du formulaire ?
    */
   refreshProfile: {
     type: Boolean,

+ 4 - 0
lang/fr.json

@@ -185,6 +185,10 @@
   "usernameSMS": "Nom d'utilisateur SMS",
   "smsSenderName": "Personnaliser le nom de l'expéditeur SMS",
   "attendance": "Absences",
+  "attendanceBookingReason": "Motif d'absence / retard",
+  "attendanceBookingReasons": "Motifs d'absence / retard",
+  "new_attendance_booking_reason": "Nouveau motif d'absence / retard",
+  "reason": "Motif",
   "notifyAdministrationAbsence": "Prévenir l'administrateur en cas d'absences consécutives",
   "sendAttendanceEmail": "Prévenir automatiquement la famille par mail en cas d'absence non justifiée",
   "sendAttendanceSms": "Prévenir automatiquement la famille par sms en cas d'absence non justifiée",

+ 23 - 0
models/Booking/AttendanceBookingReason.ts

@@ -0,0 +1,23 @@
+import ApiModel from "~/models/ApiModel";
+import {Attr, Str, Uid} from "pinia-orm/dist/decorators";
+import {IriEncoded} from "~/models/decorators";
+import Organization from "~/models/Organization/Organization";
+
+/**
+ * Motif d'absence ou de retard
+ *
+ * @see https://gitlab.2iopenservice.com/opentalent/ap2i/-/blob/develop/src/Entity/Booking/AttendanceBookingReason.php
+ */
+export default class AttendanceBookingReason extends ApiModel {
+    static entity = 'attendance_booking_reasons'
+
+    @Uid()
+    declare id: number | string
+
+    @Attr(null)
+    @IriEncoded(Organization)
+    declare organization: number | null
+
+    @Str(null)
+    declare reason: string|null
+}

+ 30 - 0
pages/parameters/attendance_booking_reasons/[id].vue

@@ -0,0 +1,30 @@
+<template>
+  <LayoutContainer>
+    <div>
+      <h2>{{ $t('attendanceBookingReason') }}</h2>
+      <UiFormEdition
+        :model="AttendanceBookingReason"
+        go-back-route="/parameters/attendances"
+      >
+        <template v-slot="{ entity }">
+          <UiInputText
+            field="reason"
+            v-model="entity.reason"
+            :rules="rules()"
+          />
+        </template>
+      </UiFormEdition>
+    </div>
+  </LayoutContainer>
+</template>
+<script setup lang="ts">
+import { useI18n } from 'vue-i18n'
+import AttendanceBookingReason from "~/models/Booking/AttendanceBookingReason";
+
+const i18n = useI18n()
+
+const rules = () => [
+  (reason: string | null) =>
+    (reason !== null && reason.length > 0) || i18n.t('please_enter_a_value'),
+]
+</script>

+ 100 - 0
pages/parameters/attendance_booking_reasons/index.vue

@@ -0,0 +1,100 @@
+<template>
+  <LayoutContainer>
+    <UiLoadingPanel v-if="pending" />
+    <div v-else>
+      <v-table>
+        <thead>
+          <tr>
+            <td>{{ $t('educationTimings') }}</td>
+            <td></td>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-if="educationTimings.length > 0" v-for="timing in educationTimings" :key="timing.id">
+            <td class="cycle-editable-cell">
+              {{ timing.timing }}
+            </td>
+            <td class="d-flex flex-row">
+              <v-btn
+                  :flat="true"
+                  icon="fa fa-pen"
+                  class="cycle-edit-icon mr-3"
+                  @click="goToEditPage(timing.id as number)"
+              />
+              <UiButtonDelete
+                  :model="EducationTiming"
+                  :entity="timing"
+                  :flat="true"
+                  class="cycle-edit-icon"
+              />
+            </td>
+          </tr>
+          <tr v-else class="theme-neutral">
+            <td><i>{{ $t('nothing_to_show')}}</i></td>
+            <td></td>
+          </tr>
+        </tbody>
+      </v-table>
+      <v-btn
+          :flat="true"
+          prepend-icon="fa fa-plus"
+          class="theme-primary mt-4"
+          @click="goToCreatePage"
+      >
+        {{ $t('add') }}
+      </v-btn>
+    </div>
+  </LayoutContainer>
+
+</template>
+
+<script setup lang="ts">
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
+import EducationTiming from '~/models/Education/EducationTiming'
+import { useRepo } from 'pinia-orm'
+import EducationTimingsRepository from '~/stores/repositories/EducationTimingsRepository'
+import type {ComputedRef} from "vue";
+import {useOrganizationProfileStore} from "~/stores/organizationProfile";
+import UrlUtils from "~/services/utils/urlUtils";
+
+const organizationProfile = useOrganizationProfileStore()
+
+if (organizationProfile.parametersId === null) {
+  throw new Error('Missing organization parameters id')
+}
+
+const { fetchCollection } = useEntityFetch()
+
+const { pending } = fetchCollection(EducationTiming)
+
+const educationTimingRepo = useRepo(EducationTimingsRepository)
+
+/**
+ * On récupère les timings via le store
+ * (sans ça, les mises à jour SSE ne seront pas prises en compte)
+ */
+const educationTimings: ComputedRef<Array<EducationTiming>> = computed(() => {
+  return educationTimingRepo.getEducationTimings()
+})
+
+const goToEditPage = (id: number) => {
+  navigateTo(UrlUtils.join('/parameters/education_timings', id))
+}
+
+const goToCreatePage = () => {
+  navigateTo('/parameters/education_timings/new')
+}
+</script>
+
+<style scoped lang="scss">
+.v-table {
+  width: 100%;
+  max-width: 800px;
+}
+
+// TODO: voir à factoriser ces styles, ptêt en faisant un component de ces boutons?
+:deep(.cycle-edit-icon .v-icon) {
+  color: rgb(var(--v-theme-primary));
+  font-size: 18px;
+}
+</style>

+ 49 - 0
pages/parameters/attendance_booking_reasons/new.vue

@@ -0,0 +1,49 @@
+<template>
+  <LayoutContainer>
+    <div>
+      <h2>{{ $t("new_attendance_booking_reason")}}</h2>
+      <UiFormCreation
+        :model="AttendanceBookingReason"
+        go-back-route="/parameters/attendances"
+        :pre-process="preProcess"
+      >
+        <template v-slot="{ entity }">
+          <v-container :fluid="true" class="container">
+            <v-row>
+              <v-col cols="12" sm="6"> </v-col>
+            </v-row>
+            <v-row>
+              <v-col cols="12" sm="6">
+                <UiInputText
+                  v-model="entity.reason"
+                  field="reason"
+                  :rules="rules()"
+                />
+              </v-col>
+            </v-row>
+          </v-container>
+        </template>
+      </UiFormCreation>
+    </div>
+  </LayoutContainer>
+</template>
+
+<script setup lang="ts">
+import { useI18n } from 'vue-i18n'
+import AttendanceBookingReason from "~/models/Booking/AttendanceBookingReason";
+import {useOrganizationProfileStore} from "#imports";
+
+const i18n = useI18n()
+
+const organizationProfile = useOrganizationProfileStore()
+
+const preProcess = (entity: AttendanceBookingReason) => {
+  entity.organization = organizationProfile.id
+  return entity
+}
+
+const rules = () => [
+  (reason: string | null) =>
+      (reason !== null && reason.length > 0) || i18n.t('please_enter_a_value'),
+]
+</script>

+ 74 - 0
pages/parameters/attendances.vue

@@ -27,6 +27,53 @@
         </v-col>
       </v-row>
     </UiForm>
+
+    <v-divider class="my-10"/>
+
+    <UiLoadingPanel v-if="attendanceBookingReasonsPending" />
+    <div v-else>
+      <v-table>
+        <thead>
+        <tr>
+          <td>{{ $t('attendanceBookingReasons') }}</td>
+          <td></td>
+        </tr>
+        </thead>
+        <tbody>
+        <tr v-if="attendanceBookingReasons.length > 0" v-for="reason in attendanceBookingReasons" :key="reason.id">
+          <td class="cycle-editable-cell">
+            {{ reason.reason }}
+          </td>
+          <td class="d-flex flex-row">
+            <v-btn
+                :flat="true"
+                icon="fa fa-pen"
+                class="cycle-edit-icon mr-3"
+                @click="goToEditPage(reason.id as number)"
+            />
+            <UiButtonDelete
+                :model="AttendanceBookingReason"
+                :entity="reason"
+                :flat="true"
+                class="cycle-edit-icon"
+            />
+          </td>
+        </tr>
+        <tr v-else class="theme-neutral">
+          <td><i>{{ $t('nothing_to_show')}}</i></td>
+          <td></td>
+        </tr>
+        </tbody>
+      </v-table>
+      <v-btn
+          :flat="true"
+          prepend-icon="fa fa-plus"
+          class="theme-primary mt-4"
+          @click="goToCreatePage"
+      >
+        {{ $t('add') }}
+      </v-btn>
+    </div>
   </LayoutContainer>
 </template>
 <script setup lang="ts">
@@ -34,6 +81,11 @@ import Parameters from '~/models/Organization/Parameters'
 import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import { useOrganizationProfileStore } from '~/stores/organizationProfile'
 import type { AsyncData } from '#app'
+import {type Collection, useRepo} from "pinia-orm";
+import type {ComputedRef} from "vue";
+import UrlUtils from "~/services/utils/urlUtils";
+import AttendanceBookingReason from "~/models/Booking/AttendanceBookingReason";
+import AttendanceBookingReasonRepository from "~/stores/repositories/AttendanceBookingReasonRepository";
 
 
 const { fetch } = useEntityFetch()
@@ -48,4 +100,26 @@ const { data: parameters, pending } = fetch(
   Parameters,
   organizationProfile.parametersId
 ) as AsyncData<Parameters, Parameters | true>
+
+const { fetchCollection } = useEntityFetch()
+
+const { pending: attendanceBookingReasonsPending } = fetchCollection(AttendanceBookingReason)
+
+const attendanceBookingReasonsRepo = useRepo(AttendanceBookingReasonRepository)
+
+/**
+ * On récupère les timings via le store
+ * (sans ça, les mises à jour SSE ne seront pas prises en compte)
+ */
+const attendanceBookingReasons: ComputedRef<Collection<AttendanceBookingReason>> = computed(() => {
+  return attendanceBookingReasonsRepo.getReasons()
+})
+
+const goToEditPage = (id: number) => {
+  navigateTo(UrlUtils.join('/parameters/attendance_booking_reasons', id))
+}
+
+const goToCreatePage = () => {
+  navigateTo('/parameters/attendance_booking_reasons/new')
+}
 </script>

+ 1 - 1
pages/parameters/education_timings/index.vue

@@ -63,7 +63,7 @@ if (organizationProfile.parametersId === null) {
   throw new Error('Missing organization parameters id')
 }
 
-const { fetch, fetchCollection } = useEntityFetch()
+const { fetchCollection } = useEntityFetch()
 
 const { pending } = fetchCollection(EducationTiming)
 

+ 1 - 1
services/layout/menuBuilder/parametersMenuBuilder.ts

@@ -26,7 +26,7 @@ export default class ParametersMenuBuilder extends AbstractMenuBuilder {
       children.push(this.createItem('educationNotations', {name: 'fas fa-graduation-cap'}, `/parameters/education_notation`))
       children.push(this.createItem('bulletin', {name: 'fas fa-file-lines'}, `/parameters/bulletin`))
       children.push(this.createItem('education_timings_breadcrumbs', {name: 'fas fa-clock'}, `/parameters/education_timings`))
-      children.push(this.createItem('attendances', {name: 'fas fa-user-times'}, `/parameters/attendances`))
+      children.push(this.createItem('attendance', {name: 'fas fa-user-times'}, `/parameters/attendances`))
       children.push(this.createItem('residenceAreas', {name: 'fas fa-location-dot'}, `/parameters/residence_areas`))
     }
 

+ 18 - 0
stores/repositories/AttendanceBookingReasonRepository.ts

@@ -0,0 +1,18 @@
+import type {Collection} from "pinia-orm";
+import BaseRepository from "~/stores/repositories/BaseRepository";
+import AttendanceBookingReason from "~/models/Booking/AttendanceBookingReason";
+
+class AttendanceBookingReasonRepository extends BaseRepository {
+    use = AttendanceBookingReason
+
+    /**
+     * On récupère les AttendanceBookingReasons via le store
+     */
+    public getReasons(): Collection<AttendanceBookingReason> {
+        return this.getQuery()
+            .get() as Collection<AttendanceBookingReason>
+    }
+
+}
+
+export default AttendanceBookingReasonRepository