Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b185accdbb | ||
| a4bda53f57 | |||
|
|
c255000a5e | ||
| b8b9368ad0 | |||
|
|
10a0ab0809 | ||
| 055f1187f9 |
@@ -26,7 +26,8 @@
|
||||
"Bash(pip3 install:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)"
|
||||
"Bash(git commit:*)",
|
||||
"Bash(python3:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
11
CLAUDE.md
11
CLAUDE.md
@@ -2,6 +2,7 @@
|
||||
|
||||
## Mandatory Rules
|
||||
- Any functional change MUST update `doc/` in the same intervention
|
||||
- Any functional change MUST update the in-app documentation (`frontend/data/documentation-content.ts`) in the same intervention
|
||||
- At the end of every feature addition or functional modification, update this CLAUDE.md to reflect new patterns, rules, or conventions introduced
|
||||
|
||||
## Commands
|
||||
@@ -84,6 +85,16 @@
|
||||
- Keep backend PHP DTOs aligned with frontend TS DTOs (`frontend/services/dto/*`)
|
||||
- Update unit tests when constructor/service signatures change
|
||||
|
||||
## In-App Documentation
|
||||
- Content: `frontend/data/documentation-content.ts` — structured TypeScript data with all user-facing documentation
|
||||
- Types: `frontend/types/documentation.ts` — DocSection, DocArticle, DocBlock
|
||||
- Composable: `frontend/composables/useDocumentation.ts` — role-based filtering (employee < site_manager < admin)
|
||||
- Components: `frontend/components/documentation/` — DocumentationPage, DocumentationSection, DocumentationArticle
|
||||
- Page: `frontend/pages/documentation.vue`
|
||||
- 3 access levels: `employee` (ROLE_SELF), `site_manager` (ROLE_USER), `admin` (ROLE_ADMIN) — cumulative (admin sees everything)
|
||||
- Each section/article has a `requiredLevel` that controls visibility
|
||||
- When adding or modifying a feature, update the corresponding section in `documentation-content.ts`
|
||||
|
||||
## Language
|
||||
- UI is in French
|
||||
- User communicates in French
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
parameters:
|
||||
app.version: '0.1.77'
|
||||
app.version: '0.1.80'
|
||||
|
||||
26
frontend/components/documentation/DocumentationArticle.vue
Normal file
26
frontend/components/documentation/DocumentationArticle.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<article :id="`doc-${article.id}`" class="scroll-mt-6">
|
||||
<h3 class="text-lg font-bold text-primary-500 mb-3">{{ article.title }}</h3>
|
||||
<div class="space-y-3">
|
||||
<template v-for="(block, idx) in article.blocks" :key="idx">
|
||||
<p v-if="block.type === 'paragraph'" class="text-sm text-neutral-700 leading-relaxed">
|
||||
{{ block.content }}
|
||||
</p>
|
||||
<ul v-else-if="block.type === 'list'" class="list-disc list-inside space-y-1 text-sm text-neutral-700 pl-2">
|
||||
<li v-for="(item, i) in block.content.split('\n')" :key="i">{{ item }}</li>
|
||||
</ul>
|
||||
<div v-else-if="block.type === 'note'" class="bg-tertiary-500 border-l-4 border-primary-500 p-3 rounded-r-md">
|
||||
<p class="text-sm text-neutral-700 leading-relaxed">{{ block.content }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DocArticle } from '~/types/documentation'
|
||||
|
||||
defineProps<{
|
||||
article: DocArticle
|
||||
}>()
|
||||
</script>
|
||||
67
frontend/components/documentation/DocumentationPage.vue
Normal file
67
frontend/components/documentation/DocumentationPage.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="h-full flex gap-8">
|
||||
<!-- Table des matières -->
|
||||
<nav class="w-64 flex-shrink-0 overflow-y-auto pr-4 border-r border-neutral-200">
|
||||
<h1 class="text-xl font-bold text-primary-500 mb-6">Documentation</h1>
|
||||
<div v-for="section in visibleSections" :key="section.id" class="mb-4">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<Icon :name="section.icon" size="18" class="text-neutral-500"/>
|
||||
<span class="text-sm font-semibold text-neutral-700">{{ section.title }}</span>
|
||||
</div>
|
||||
<ul class="pl-7 space-y-0.5">
|
||||
<li v-for="article in section.articles" :key="article.id">
|
||||
<button
|
||||
class="text-xs text-neutral-500 hover:text-primary-500 text-left w-full py-0.5 transition-colors"
|
||||
:class="activeArticleId === article.id ? 'text-primary-500 font-bold' : ''"
|
||||
@click="scrollToArticle(article.id)"
|
||||
>
|
||||
{{ article.title }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Contenu -->
|
||||
<div ref="contentRef" class="flex-1 overflow-y-auto pr-4">
|
||||
<DocumentationSection
|
||||
v-for="section in visibleSections"
|
||||
:key="section.id"
|
||||
:section="section"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { visibleSections, activeArticleId, scrollToArticle } = useDocumentation()
|
||||
const contentRef = ref<HTMLElement | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
if (!contentRef.value) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
const id = entry.target.id.replace('doc-', '')
|
||||
activeArticleId.value = id
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
root: contentRef.value,
|
||||
rootMargin: '-10% 0px -80% 0px',
|
||||
threshold: 0,
|
||||
},
|
||||
)
|
||||
|
||||
nextTick(() => {
|
||||
const articles = contentRef.value?.querySelectorAll('[id^="doc-"]')
|
||||
articles?.forEach(el => observer.observe(el))
|
||||
})
|
||||
|
||||
onUnmounted(() => observer.disconnect())
|
||||
})
|
||||
</script>
|
||||
23
frontend/components/documentation/DocumentationSection.vue
Normal file
23
frontend/components/documentation/DocumentationSection.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<section class="mb-10">
|
||||
<div class="flex items-center gap-3 border-b-2 border-primary-500 pb-3 mb-6">
|
||||
<Icon :name="section.icon" size="28" class="text-primary-500"/>
|
||||
<h2 class="text-xl font-bold text-primary-500">{{ section.title }}</h2>
|
||||
</div>
|
||||
<div class="space-y-8 pl-2">
|
||||
<DocumentationArticle
|
||||
v-for="article in section.articles"
|
||||
:key="article.id"
|
||||
:article="article"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DocSection } from '~/types/documentation'
|
||||
|
||||
defineProps<{
|
||||
section: DocSection
|
||||
}>()
|
||||
</script>
|
||||
@@ -30,7 +30,7 @@
|
||||
class="rounded-md bg-primary-500 px-8 py-2 font-bold text-white hover:bg-primary-600"
|
||||
@click="openPaymentDrawer"
|
||||
>
|
||||
+ Payer les RRT
|
||||
+ Payer les RTT
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
39
frontend/composables/useDocumentation.ts
Normal file
39
frontend/composables/useDocumentation.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { documentationSections } from '~/data/documentation-content'
|
||||
import type { DocAccessLevel, DocSection } from '~/types/documentation'
|
||||
|
||||
const LEVEL_HIERARCHY: Record<DocAccessLevel, number> = {
|
||||
employee: 0,
|
||||
site_manager: 1,
|
||||
admin: 2,
|
||||
}
|
||||
|
||||
function getUserLevel(roles: string[]): number {
|
||||
if (roles.includes('ROLE_ADMIN') || roles.includes('ROLE_SUPER_ADMIN')) return 2
|
||||
if (roles.includes('ROLE_USER')) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
export function useDocumentation() {
|
||||
const auth = useAuthStore()
|
||||
const userLevel = computed(() => getUserLevel(auth.user?.roles ?? []))
|
||||
|
||||
const visibleSections = computed<DocSection[]>(() => {
|
||||
return documentationSections
|
||||
.filter(s => LEVEL_HIERARCHY[s.requiredLevel] <= userLevel.value)
|
||||
.map(s => ({
|
||||
...s,
|
||||
articles: s.articles.filter(a => LEVEL_HIERARCHY[a.requiredLevel] <= userLevel.value),
|
||||
}))
|
||||
.filter(s => s.articles.length > 0)
|
||||
})
|
||||
|
||||
const activeArticleId = ref<string | null>(null)
|
||||
|
||||
const scrollToArticle = (articleId: string) => {
|
||||
activeArticleId.value = articleId
|
||||
const el = document.getElementById(`doc-${articleId}`)
|
||||
el?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
|
||||
return { visibleSections, activeArticleId, scrollToArticle }
|
||||
}
|
||||
568
frontend/data/documentation-content.ts
Normal file
568
frontend/data/documentation-content.ts
Normal file
@@ -0,0 +1,568 @@
|
||||
import type { DocSection } from '~/types/documentation'
|
||||
|
||||
export const documentationSections: DocSection[] = [
|
||||
// ============================================================
|
||||
// EMPLOYEE LEVEL
|
||||
// ============================================================
|
||||
{
|
||||
id: 'connexion',
|
||||
title: 'Connexion et navigation',
|
||||
requiredLevel: 'employee',
|
||||
icon: 'mdi:login',
|
||||
articles: [
|
||||
{
|
||||
id: 'login',
|
||||
title: 'Se connecter',
|
||||
requiredLevel: 'employee',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Pour accéder à l\'application, rendez-vous sur la page de connexion et saisissez vos identifiants.' },
|
||||
{ type: 'list', content: 'Saisissez votre nom d\'utilisateur\nSaisissez votre mot de passe\nCliquez sur le bouton "Connexion"' },
|
||||
{ type: 'note', content: 'Si vous ne parvenez pas à vous connecter, contactez votre administrateur RH. Votre compte a peut-être été verrouillé.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'navigation',
|
||||
title: 'Naviguer dans la vue jour',
|
||||
requiredLevel: 'employee',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'La vue jour est votre écran principal. Elle affiche les heures de travail pour une date donnée.' },
|
||||
{ type: 'list', content: 'Boutons "Hier" / "Aujourd\'hui" / "Demain" pour naviguer rapidement\nSélecteur de date pour choisir une date spécifique\nFiltrage par site si vous avez accès à plusieurs sites' },
|
||||
{ type: 'paragraph', content: 'Seuls les employés ayant un contrat actif à la date sélectionnée sont affichés.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'perimetre',
|
||||
title: 'Périmètre d\'accès',
|
||||
requiredLevel: 'employee',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Votre accès dépend du rôle qui vous a été attribué par l\'administrateur.' },
|
||||
{ type: 'list', content: 'Employé : accès à la saisie de ses propres heures uniquement\nChef de site : accès aux heures des employés de ses sites autorisés + validation\nAdministrateur : accès complet à toutes les fonctionnalités' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'saisie-heures',
|
||||
title: 'Saisie des heures',
|
||||
requiredLevel: 'employee',
|
||||
icon: 'mdi:clock-time-four-outline',
|
||||
articles: [
|
||||
{
|
||||
id: 'saisie-time',
|
||||
title: 'Mode horaire (TIME)',
|
||||
requiredLevel: 'employee',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'En mode horaire, vous saisissez vos heures via des créneaux matin, après-midi et soir.' },
|
||||
{ type: 'list', content: 'Matin : heure de début et heure de fin\nAprès-midi : heure de début et heure de fin\nSoir : heure de début et heure de fin' },
|
||||
{ type: 'paragraph', content: 'Le sélecteur de temps fonctionne par tranches de 15 minutes (00, 15, 30, 45). La saisie libre est possible mais sera corrigée automatiquement.' },
|
||||
{ type: 'note', content: 'Les calculs sont mis à jour automatiquement : heures de jour (06:00–21:00), heures de nuit (00:00–06:00 et 21:00–24:00) et total.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'saisie-presence',
|
||||
title: 'Mode présence (PRESENCE)',
|
||||
requiredLevel: 'employee',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'En mode présence (contrats forfait), vous indiquez simplement si vous étiez présent le matin et/ou l\'après-midi.' },
|
||||
{ type: 'list', content: 'Cochez "Présent matin" pour indiquer une demi-journée de travail le matin\nCochez "Présent après-midi" pour indiquer une demi-journée l\'après-midi\nChaque demi-journée cochée compte pour 0.5 jour de présence' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'comprendre-calculs',
|
||||
title: 'Comprendre les calculs affichés',
|
||||
requiredLevel: 'employee',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Les colonnes de calcul sont mises à jour automatiquement en fonction de votre saisie.' },
|
||||
{ type: 'list', content: 'Jour : total des heures dans la plage 06:00–21:00\nNuit : total des heures dans les plages 00:00–06:00 et 21:00–24:00\nTotal : somme des heures de jour et de nuit' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'saisie-conducteurs',
|
||||
title: 'Saisie conducteurs',
|
||||
requiredLevel: 'employee',
|
||||
icon: 'mdi:truck-outline',
|
||||
articles: [
|
||||
{
|
||||
id: 'conducteur-heures',
|
||||
title: 'Saisie des heures conducteur',
|
||||
requiredLevel: 'employee',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Les conducteurs disposent d\'un écran dédié accessible via le menu "Heures Conducteurs". Ils n\'apparaissent pas sur l\'écran classique des heures.' },
|
||||
{ type: 'list', content: 'Heures de jour : durée au format HH:MM\nHeures de nuit : durée au format HH:MM\nHeures atelier : durée au format HH:MM\nTotal : calculé automatiquement (jour + nuit + atelier)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'conducteur-indemnites',
|
||||
title: 'Indemnités conducteur',
|
||||
requiredLevel: 'employee',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'En plus des heures, vous pouvez cocher les indemnités correspondant à votre journée.' },
|
||||
{ type: 'list', content: 'Petit déjeuner\nDéjeuner\nDîner\nNuitée' },
|
||||
{ type: 'paragraph', content: 'La même logique de validation s\'applique que pour les heures classiques.' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'absences-validations',
|
||||
title: 'Absences et validations',
|
||||
requiredLevel: 'employee',
|
||||
icon: 'mdi:information-outline',
|
||||
articles: [
|
||||
{
|
||||
id: 'comprendre-absences',
|
||||
title: 'Comprendre les absences affichées',
|
||||
requiredLevel: 'employee',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Quand une absence est posée sur votre journée, elle apparaît dans la colonne dédiée avec un fond coloré selon le type d\'absence.' },
|
||||
{ type: 'list', content: 'Absence du matin (AM) : verrouille le créneau matin\nAbsence de l\'après-midi (PM) : verrouille les créneaux après-midi et soir\nAbsence journée complète : verrouille tous les créneaux' },
|
||||
{ type: 'note', content: 'Vous ne pouvez pas modifier les créneaux horaires verrouillés par une absence. Seul un administrateur peut retirer ou modifier l\'absence.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'comprendre-validations',
|
||||
title: 'Comprendre les validations',
|
||||
requiredLevel: 'employee',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Vos heures passent par un processus de double validation avant d\'être définitivement enregistrées.' },
|
||||
{ type: 'list', content: 'Validation chef de site : votre chef de site vérifie et valide vos heures. La ligne est alors verrouillée pour vous.\nValidation RH : l\'administrateur RH valide définitivement. La ligne est complètement verrouillée.' },
|
||||
{ type: 'paragraph', content: 'Une fois validée, vous ne pouvez plus modifier la ligne. Si une correction est nécessaire, contactez votre chef de site ou l\'administrateur RH.' },
|
||||
{ type: 'note', content: 'Toute vraie modification effectuée par un administrateur remet automatiquement les deux validations à zéro.' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// SITE MANAGER LEVEL
|
||||
// ============================================================
|
||||
{
|
||||
id: 'validation-site',
|
||||
title: 'Validation de site',
|
||||
requiredLevel: 'site_manager',
|
||||
icon: 'mdi:check-decagram-outline',
|
||||
articles: [
|
||||
{
|
||||
id: 'role-chef-site',
|
||||
title: 'Rôle du chef de site',
|
||||
requiredLevel: 'site_manager',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'En tant que chef de site, vous êtes responsable de la vérification et de la validation des heures saisies par les employés de votre site.' },
|
||||
{ type: 'paragraph', content: 'Le workflow de validation suit un circuit en 3 étapes : l\'employé saisit ses heures → le chef de site valide → l\'admin RH valide définitivement.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'validation-individuelle',
|
||||
title: 'Validation individuelle',
|
||||
requiredLevel: 'site_manager',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Pour valider une ligne d\'heures individuellement :' },
|
||||
{ type: 'list', content: 'Cochez la case de validation site sur la ligne de l\'employé\nLa ligne est immédiatement verrouillée pour l\'employé\nL\'administrateur RH peut toujours corriger une ligne que vous avez validée' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'validation-masse',
|
||||
title: 'Validation en masse',
|
||||
requiredLevel: 'site_manager',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Pour gagner du temps, vous pouvez valider toutes les lignes en une seule action.' },
|
||||
{ type: 'list', content: 'Cliquez sur le bouton de validation en masse\nToutes les lignes de la date affichée sont validées d\'un coup\nUtile quand toutes les saisies sont correctes' },
|
||||
{ type: 'note', content: 'Quand toutes les lignes de votre site sont validées pour une date donnée, les administrateurs RH reçoivent automatiquement une notification.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'difference-validations',
|
||||
title: 'Validation site vs validation RH',
|
||||
requiredLevel: 'site_manager',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Il est important de comprendre la différence entre les deux niveaux de validation.' },
|
||||
{ type: 'list', content: 'Validation site : verrouille la ligne pour les employés, mais l\'admin RH peut encore modifier\nValidation RH : verrouillage complet, seul l\'admin peut retirer cette validation\nLe chef de site ne voit pas et ne peut pas agir sur la validation RH' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// ADMIN LEVEL
|
||||
// ============================================================
|
||||
{
|
||||
id: 'administration',
|
||||
title: 'Administration',
|
||||
requiredLevel: 'admin',
|
||||
icon: 'mdi:cog-outline',
|
||||
articles: [
|
||||
{
|
||||
id: 'gestion-sites',
|
||||
title: 'Gestion des sites',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Les sites organisent les employés et les accès dans l\'application. Chaque site possède un nom et une couleur utilisée dans toute l\'interface.' },
|
||||
{ type: 'list', content: 'Créer, modifier ou supprimer un site depuis le menu "Sites"\nL\'ordre d\'affichage est modifiable par glisser-déposer\nLa couleur du site est utilisée pour identifier visuellement les employés' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'gestion-types-absence',
|
||||
title: 'Gestion des types d\'absence',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Les types d\'absence définissent les catégories disponibles lors de la pose d\'une absence.' },
|
||||
{ type: 'list', content: 'Code : identifiant court (max 10 caractères), ex: C, M, AT\nLibellé : nom affiché, ex: Congé, Maladie, Accident du travail\nCouleur : code couleur pour le calendrier et la vue jour\nOption "Compté comme travaillé" : si activé, l\'absence crédite des heures en mode TIME' },
|
||||
{ type: 'note', content: 'L\'option "Compté comme travaillé" impacte le calcul des heures supplémentaires. En mode TIME, les minutes sont créditées selon le contrat. En mode PRESENCE, aucun crédit n\'est appliqué.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'gestion-utilisateurs',
|
||||
title: 'Gestion des utilisateurs',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Chaque personne qui se connecte à l\'application a un compte utilisateur distinct de sa fiche employé.' },
|
||||
{ type: 'list', content: 'Nom d\'utilisateur : unique, sert de login\nMot de passe : défini à la création, modifiable\nRôle : Admin (accès complet), User (chef de site), Self (employé)\nSites autorisés : pour les chefs de site, définit leur périmètre\nAssociation employé : lie le compte à une fiche employé\nVerrouillage : un compte verrouillé ne peut plus se connecter' },
|
||||
{ type: 'note', content: 'Il n\'est pas possible de supprimer un utilisateur (sécurité). Pour bloquer l\'accès, utilisez le verrouillage de compte.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'taches-automatiques',
|
||||
title: 'Tâches automatiques (crons)',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Deux tâches automatiques s\'exécutent quotidiennement pour gérer le report des compteurs.' },
|
||||
{ type: 'list', content: 'Report congés (02h10) : déclenche le report des congés payés le 1er juin (CDI/CDD) et le 1er janvier (forfait)\nReport RTT (02h15) : déclenche le report du solde RTT le 1er juin' },
|
||||
{ type: 'note', content: 'Ces tâches sont idempotentes : si elles s\'exécutent plusieurs fois, aucun doublon n\'est créé.' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'employes-contrats',
|
||||
title: 'Employés et contrats',
|
||||
requiredLevel: 'admin',
|
||||
icon: 'mdi:account-group-outline',
|
||||
articles: [
|
||||
{
|
||||
id: 'liste-employes',
|
||||
title: 'Liste et recherche d\'employés',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'La page Employés affiche tous les employés sous forme de cartes.' },
|
||||
{ type: 'list', content: 'Recherche par nom\nFiltrage par site (multi-sélection)\nFiltrage par statut de contrat : "Avec contrat" (défaut), "Sans contrat", "Tous"\n"Avec contrat" = employés ayant une période de contrat active à la date du jour' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'creation-employe',
|
||||
title: 'Créer un employé',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'La création d\'un employé se fait via le drawer d\'ajout.' },
|
||||
{ type: 'list', content: 'Champs : prénom, nom, site\nNature du contrat : CDI, CDD ou INTERIM\nType de contrat / temps de travail (Forfait, 35h, 39h, etc.)\nDate de début (obligatoire)\nDate de fin (obligatoire pour CDD et INTERIM)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'types-contrat',
|
||||
title: 'Types de contrat',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Le type de contrat détermine le mode de suivi et les règles de calcul appliquées.' },
|
||||
{ type: 'list', content: 'FORFAIT : suivi en jours (mode PRESENCE), base 218 jours/an\n35 HEURES : suivi horaire (mode TIME), 35h/semaine\n39 HEURES : suivi horaire (mode TIME), 39h/semaine\nCUSTOM : heures personnalisées (ex: 4h, 20h), 1h sup = 1h récup sans bonus\nINTERIM : travail temporaire, pas de récupération ni de congés gérés' },
|
||||
{ type: 'note', content: 'Le mode de suivi (TIME ou PRESENCE) est lié au type de contrat et ne peut pas être modifié indépendamment.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'suivi-contrat',
|
||||
title: 'Suivi contrat et historique',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'L\'onglet "Suivi contrat" sur la fiche employé affiche l\'historique complet des périodes de contrat.' },
|
||||
{ type: 'list', content: 'Chaque ligne : nature (CDI/CDD/INTERIM), type de contrat, date début, date fin ou "En cours"\nAjouter un contrat : disponible uniquement si le contrat en cours est clôturé\nClôturer un contrat : définir la date de fin + option "Solde de tout compte"\nSuspension : ajouter une période de suspension avec dates et commentaire' },
|
||||
{ type: 'note', content: 'La case "Soldé dans le solde de tout compte" remet le report des congés à 0 pour l\'exercice suivant.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'statut-conducteur',
|
||||
title: 'Statut conducteur',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Le statut conducteur est un flag activé sur une période de contrat. Un employé peut changer de statut conducteur selon la période.' },
|
||||
{ type: 'paragraph', content: 'Un employé conducteur apparaît uniquement sur l\'écran "Heures Conducteurs" et non sur l\'écran "Heures" classique.' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'double-validation',
|
||||
title: 'Saisie et double validation',
|
||||
requiredLevel: 'admin',
|
||||
icon: 'mdi:shield-check-outline',
|
||||
articles: [
|
||||
{
|
||||
id: 'validation-rh',
|
||||
title: 'Validation RH',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'La validation RH est le niveau de validation le plus élevé, réservé aux administrateurs.' },
|
||||
{ type: 'list', content: 'Verrouille complètement la ligne (heures et absences)\nSeul un administrateur peut retirer cette validation\nPeut être appliquée individuellement ou en masse' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'regles-reinitialisation',
|
||||
title: 'Règles de réinitialisation',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Les validations sont automatiquement réinitialisées dans certaines conditions.' },
|
||||
{ type: 'list', content: 'Toute vraie modification d\'une ligne remet les deux validations (site et RH) à faux\nUn enregistrement sans changement réel préserve les validations existantes\nLa date de modification est mise à jour uniquement quand un employé modifie ses propres heures' },
|
||||
{ type: 'note', content: 'La date de modification est visible uniquement par les administrateurs, sous le nom de l\'employé dans la vue jour.' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'vue-semaine-hs',
|
||||
title: 'Vue semaine et heures supplémentaires',
|
||||
requiredLevel: 'admin',
|
||||
icon: 'mdi:calendar-week',
|
||||
articles: [
|
||||
{
|
||||
id: 'vue-semaine',
|
||||
title: 'Vue semaine',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'La vue semaine est réservée aux administrateurs. Elle affiche une synthèse hebdomadaire par employé avec les heures supplémentaires calculées.' },
|
||||
{ type: 'list', content: 'Filtrage par site et par employé\nDétail par jour avec totaux hebdomadaires\nColonnes de calcul : base, heures sup 25%, heures sup 50%, total récupération' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'calcul-hs',
|
||||
title: 'Calcul des heures supplémentaires',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Les règles de calcul des heures supplémentaires dépendent du type de contrat.' },
|
||||
{ type: 'list', content: 'Contrats ≤ 35h : +25% de 35h à 43h, +50% au-delà de 43h\nContrats ≥ 39h : +25% de 39h à 43h, +50% au-delà de 43h\nContrats CUSTOM (4h, 25h, etc.) : 1h supplémentaire = 1h de récupération, pas de bonus\nINTERIM : aucune récupération, aucun bonus' },
|
||||
{ type: 'note', content: 'En cas de déficit hebdomadaire (heures travaillées < heures contrat), le déficit est déduit du cumul RTT : d\'abord des heures à 50%, puis des heures à 25%.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'vue-semaine-conducteurs',
|
||||
title: 'Vue semaine conducteurs',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'La vue semaine conducteurs affiche des colonnes spécifiques.' },
|
||||
{ type: 'list', content: 'Totaux jour / nuit / atelier par jour et par semaine\nPanier de nuit (PN) : affiché quand heures nuit > heures jour OU nuit ≥ 4h\nCompteurs hebdomadaires : petit déjeuner, déjeuner, dîner, nuitée\nRTT calculé sur jour + nuit + atelier (au lieu des créneaux classiques)' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'absences-calendrier',
|
||||
title: 'Absences et calendrier',
|
||||
requiredLevel: 'admin',
|
||||
icon: 'mdi:calendar-blank',
|
||||
articles: [
|
||||
{
|
||||
id: 'poser-absence',
|
||||
title: 'Poser une absence',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Les absences peuvent être posées depuis la vue jour des heures ou depuis le calendrier.' },
|
||||
{ type: 'list', content: 'Journée complète : efface toutes les plages horaires\nDemi-journée matin (AM) : efface le créneau matin\nDemi-journée après-midi (PM) : efface les créneaux après-midi et soir' },
|
||||
{ type: 'paragraph', content: 'Les absences sont stockées par jour : une absence de plusieurs jours est automatiquement découpée en entrées quotidiennes.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'effet-absences-heures',
|
||||
title: 'Effet sur les heures',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'L\'impact d\'une absence sur les heures dépend du type d\'absence et du mode de suivi.' },
|
||||
{ type: 'list', content: 'Standard : efface les créneaux horaires correspondants\nSi "Compté comme travaillé" en mode TIME : crédite des minutes selon le contrat actif\nSi "Compté comme travaillé" en mode PRESENCE : aucun crédit (seules les cases cochées comptent)' },
|
||||
{ type: 'note', content: 'Les absences comptées comme travaillées impactent le calcul des heures supplémentaires et du RTT.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'calendrier-mensuel',
|
||||
title: 'Calendrier mensuel',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Le calendrier offre une vue d\'ensemble mensuelle des absences de tous les employés.' },
|
||||
{ type: 'list', content: 'Code couleur par type d\'absence\nDemi-journée : affichage en dégradé diagonal\nJournée complète : fond plein\nJours fériés : fond bleu clair\nFiltres par site et par employé\nNavigation par mois (précédent / suivant)' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'conges-payes',
|
||||
title: 'Congés payés',
|
||||
requiredLevel: 'admin',
|
||||
icon: 'mdi:umbrella-beach-outline',
|
||||
articles: [
|
||||
{
|
||||
id: 'regles-cdi-cdd',
|
||||
title: 'Règles CDI/CDD non-forfait',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Pour les contrats CDI et CDD (hors forfait), l\'exercice de congés va du 1er juin (N-1) au 31 mai (N).' },
|
||||
{ type: 'list', content: 'Acquisition annuelle : 25 jours + 5 samedis\nAcquisition mensuelle : 2,08 jours + 0,42 samedi par mois\nProratisation en cas de début/fin ou suspension en cours de mois\nContrat 4h : 10 jours annuels, 0 samedi, 0,83 jour/mois' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'regles-forfait',
|
||||
title: 'Règles FORFAIT',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Pour les contrats forfait, l\'exercice suit l\'année civile (1er janvier au 31 décembre).' },
|
||||
{ type: 'list', content: 'Calcul : jours ouvrés de l\'année − 218 + bonus weekend/férié\nBonus : 1 jour par jour travaillé un weekend ou jour férié (0.5 si demi-journée)\nPas de samedis\nPas de jours en cours d\'acquisition' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'maladie-longue',
|
||||
title: 'Arrêt maladie long',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'En cas d\'arrêt maladie de plus d\'un mois, les règles d\'acquisition sont modifiées.' },
|
||||
{ type: 'list', content: 'Premier mois de maladie : acquisition normale\nAprès le premier mois : acquisition réduite (facteur 0,80)\nDétection automatique à partir des absences MALADIE consécutives (tolérance de gap ≤ 3 jours)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'report-conges',
|
||||
title: 'Report annuel et rollover',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Le reliquat de congés de l\'exercice précédent est automatiquement reporté dans les acquis du nouvel exercice.' },
|
||||
{ type: 'list', content: 'Report automatique le 1er juin (CDI/CDD non-forfait) ou 1er janvier (forfait)\nSi "Solde de tout compte" coché sur le contrat clôturé : report remis à 0\nJours fractionnés : saisie manuelle par la RH, ajoutés aux acquis' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'consommation-conges',
|
||||
title: 'Règle de consommation',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Les absences s\'imputent selon un ordre précis.' },
|
||||
{ type: 'list', content: 'D\'abord sur les acquis (report N-1)\nPuis sur les jours en cours d\'acquisition\nEn cours d\'acquisition peut devenir négatif temporairement (se reconstitue avec les acquisitions suivantes)' },
|
||||
{ type: 'paragraph', content: 'Compteurs visibles sur l\'onglet Congé de la fiche employé : acquis, en cours d\'acquisition, pris, restant.' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'rtt',
|
||||
title: 'RTT (Récupération de Temps de Travail)',
|
||||
requiredLevel: 'admin',
|
||||
icon: 'mdi:timer-sand',
|
||||
articles: [
|
||||
{
|
||||
id: 'rtt-principe',
|
||||
title: 'Principe et exercice RTT',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Le RTT correspond aux heures supplémentaires accumulées, converties en temps de récupération. L\'exercice RTT va du 1er juin (N-1) au 31 mai (N).' },
|
||||
{ type: 'paragraph', content: 'L\'onglet RTT sur la fiche employé affiche le détail hebdomadaire regroupé par mois, avec un compteur global en heures (1 jour = 7h = 420 minutes).' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'rtt-compteurs',
|
||||
title: 'Compteurs RTT',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'list', content: 'Report N-1 : solde de l\'exercice précédent\nAcquis : cumul des heures supplémentaires de l\'exercice en cours\nDisponible : report + acquis − payé\nPayé : RTT convertis en salaire (soustraits du disponible)' },
|
||||
{ type: 'note', content: 'Les contrats INTERIM et le mode PRESENCE n\'accumulent pas de RTT (affiché à 0).' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'rtt-paiement',
|
||||
title: 'Paiement RTT',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'L\'administrateur RH peut enregistrer un paiement RTT depuis l\'onglet RTT de la fiche employé.' },
|
||||
{ type: 'list', content: 'Saisie : mois, nombre de minutes, taux (25% ou 50%)\nPlusieurs paiements possibles par mois\nLes heures payées sont soustraites du solde disponible' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'rtt-semaines-mois',
|
||||
title: 'Attribution des semaines aux mois',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Chaque semaine ISO est attribuée à un seul mois dans le tableau RTT.' },
|
||||
{ type: 'list', content: 'Une semaine est attribuée au mois qui contient son samedi\nSi le samedi tombe en début de mois suivant, la semaine est dans ce mois suivant' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'frais-primes-observations',
|
||||
title: 'Frais, primes et observations',
|
||||
requiredLevel: 'admin',
|
||||
icon: 'mdi:account-cash-outline',
|
||||
articles: [
|
||||
{
|
||||
id: 'frais',
|
||||
title: 'Onglet Frais',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'L\'onglet Frais sur la fiche employé permet de saisir les frais kilométriques et les montants associés.' },
|
||||
{ type: 'list', content: 'Mois : obligatoire\nKilomètres : nombre de km (optionnel)\nMontant : en euros (optionnel)\nCommentaire : optionnel\nDeux justificatifs PDF distincts : un pour les km, un pour le montant' },
|
||||
{ type: 'note', content: 'Au moins un des deux champs (kilomètres ou montant) doit être supérieur à 0. Un seul enregistrement par mois par employé.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'primes',
|
||||
title: 'Onglet Prime',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'list', content: 'Mois : obligatoire\nMontant en euros : obligatoire\nCommentaire : optionnel\nUne seule prime par mois par employé' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'observations',
|
||||
title: 'Onglet Observation',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'list', content: 'Mois : obligatoire\nTexte d\'observation : obligatoire\nUne seule observation par mois par employé\nNote libre pour le suivi RH' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'exports',
|
||||
title: 'Exports et impressions',
|
||||
requiredLevel: 'admin',
|
||||
icon: 'mdi:file-pdf-box',
|
||||
articles: [
|
||||
{
|
||||
id: 'export-recap-conges',
|
||||
title: 'Export récap. congés',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Génère un PDF A4 portrait récapitulant les congés de tous les employés actifs.' },
|
||||
{ type: 'list', content: 'Accessible depuis la page Employés (bouton "Export récap. congés")\nGénère un PDF à la date du jour\nDonnées groupées par site\nColonnes : nom, contrat, CP N-1 restant, samedi restant, CP N, RTT' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'export-recap-salaire',
|
||||
title: 'Récapitulatif salaire',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Génère un PDF A4 paysage avec le détail mensuel pour la paie.' },
|
||||
{ type: 'list', content: 'Sélecteur de mois (défaut = mois courant)\nDonnées groupées par site\nColonnes : nom, base contrat, jours de présence cadre, heures de nuit, panier de nuit, heures RTT payées, congés (nombre + dates), maladie/AT (nombre + dates), primes conducteur (PDJ, repas, nuitée, samedi), observations' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'impression-absences',
|
||||
title: 'Impression absences',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Génère un PDF A3 paysage du calendrier d\'absences avec des filtres.' },
|
||||
{ type: 'list', content: 'Filtres : période (du/au), sites, nature de contrat, type de contrat\nTous les filtres sont cochés par défaut\nCalendrier coloré par type d\'absence' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'export-heures-annuelles',
|
||||
title: 'Export heures annuelles',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Génère un PDF par employé avec le détail jour par jour de ses heures sur une année.' },
|
||||
{ type: 'list', content: 'Accessible depuis la fiche employé (bouton imprimante)\nChoix de l\'année civile (janvier à décembre)\nColonnes adaptées au mode de suivi (TIME, PRESENCE, conducteur)\nSections séparées en cas de changement de contrat en cours d\'année' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -95,6 +95,16 @@
|
||||
<Icon name="mdi:clipboard-text-clock-outline" size="24"/>
|
||||
<p>Journal</p>
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
to="/documentation"
|
||||
class="flex items-center gap-2 py-3 text-md text-black hover:bg-tertiary-500 hover:text-primary-500"
|
||||
:class="route.path.startsWith('/documentation')
|
||||
? 'bg-tertiary-500 text-primary-500 font-bold'
|
||||
: ''"
|
||||
>
|
||||
<Icon name="mdi:book-open-page-variant-outline" size="24"/>
|
||||
<p>Documentation</p>
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
|
||||
<div class="flex flex-col gap-2 items-center p-4">
|
||||
|
||||
9
frontend/pages/documentation.vue
Normal file
9
frontend/pages/documentation.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<DocumentationPage/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({
|
||||
title: 'Documentation',
|
||||
})
|
||||
</script>
|
||||
21
frontend/types/documentation.ts
Normal file
21
frontend/types/documentation.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export type DocAccessLevel = 'employee' | 'site_manager' | 'admin'
|
||||
|
||||
export interface DocBlock {
|
||||
type: 'paragraph' | 'list' | 'note'
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface DocArticle {
|
||||
id: string
|
||||
title: string
|
||||
requiredLevel: DocAccessLevel
|
||||
blocks: DocBlock[]
|
||||
}
|
||||
|
||||
export interface DocSection {
|
||||
id: string
|
||||
title: string
|
||||
requiredLevel: DocAccessLevel
|
||||
icon: string
|
||||
articles: DocArticle[]
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Repository\AuditLogRepository;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
@@ -59,7 +60,7 @@ class AuditLogProvider implements ProviderInterface
|
||||
'description' => $log->getDescription(),
|
||||
'changes' => $log->getChanges(),
|
||||
'affectedDate' => $log->getAffectedDate()?->format('Y-m-d'),
|
||||
'createdAt' => $log->getCreatedAt()->format('Y-m-d H:i:s'),
|
||||
'createdAt' => $log->getCreatedAt()->setTimezone(new DateTimeZone('Europe/Paris'))->format('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -294,17 +294,28 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
||||
}
|
||||
|
||||
// Week spans two months — split proportionally by daily worked minutes
|
||||
$monthMinutes = [];
|
||||
$monthMinutes = [];
|
||||
$monthWeekdays = [];
|
||||
foreach ($detail->dailyMinutes as $date => $mins) {
|
||||
$m = (int) new DateTimeImmutable($date)->format('n');
|
||||
$monthMinutes[$m] = ($monthMinutes[$m] ?? 0) + $mins;
|
||||
$isoDay = (int) new DateTimeImmutable($date)->format('N');
|
||||
if ($isoDay < 6) {
|
||||
$monthWeekdays[$m] = ($monthWeekdays[$m] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
$totalWorked = array_sum($monthMinutes);
|
||||
$totalWorked = array_sum($monthMinutes);
|
||||
$totalWeekdays = array_sum($monthWeekdays);
|
||||
|
||||
foreach ([$startMonth, $endMonth] as $month) {
|
||||
$portion = $monthMinutes[$month] ?? 0;
|
||||
$ratio = $totalWorked > 0 ? $portion / $totalWorked : 0.0;
|
||||
if ($totalWorked > 0) {
|
||||
$ratio = ($monthMinutes[$month] ?? 0) / $totalWorked;
|
||||
} elseif ($totalWeekdays > 0) {
|
||||
$ratio = ($monthWeekdays[$month] ?? 0) / $totalWeekdays;
|
||||
} else {
|
||||
$ratio = 0.0;
|
||||
}
|
||||
|
||||
$result[] = new EmployeeRttWeekSummary(
|
||||
month: $month,
|
||||
|
||||
Reference in New Issue
Block a user