diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b68915..aa6dc4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,36 @@ Toutes les évolutions notables de **Dalibi** sont consignées ici. Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le versionnage respecte [SemVer](https://semver.org/lang/fr/). +## [1.1.0] — 2026-09-03 + +### Ajouté +- **Jeu de démonstration complet** (`DemoSeeder`) : une année scolaire vivante sur toutes + les classes actives (au moins 20 élèves par classe), avec dossiers d'élèves complets, + programme et affectations des enseignants, évaluations notées et moyennes par matière, + bulletins figés, facturation et paiements, présences, emploi du temps, calendrier, + cycles de paie et dossiers courants (justificatifs, réclamations, primes, documents). + Noms, prénoms et villes réels du Togo (divisions administratives officielles), + recrutement pondéré autour de Lomé. Seeder idempotent, déterministe, et interdit en + production. Lancement : `php artisan db:seed --class=DemoSeeder`. + +### Corrigé +- **Bulletin — colonne Composition toujours vide** : la colonne de source « composition » + était résolue sur une clé absente du bulletin figé (la valeur y est stockée sous + « compo »). La colonne restait vide **même quand la note existait**, quelle que soit la + période ou le barème. Correctif d'affichage : aucun bulletin à régénérer. +- **Bulletin — logo de l'en-tête surdimensionné** : les styles de l'en-tête ministériel + (dont le plafond de taille du logo) n'étaient pas embarqués dans le CSS du bulletin ; le + logo s'affichait à sa taille naturelle et écrasait l'en-tête. +- **Types d'évaluation — catégorie de la « Composition »** : lorsqu'elle avait dérivé vers + « contrôle continu », la composition était diluée dans la note de classe et la + pondération Classe/Compo du barème était ignorée. Rejouer `ReferenceDataSeeder` corrige + la donnée, puis régénérer les bulletins concernés. + +### Modifié +- **Bulletin — la colonne « Classe »** (contrôle continu) s'intitule désormais « Devoir » + par défaut. Seul l'affichage change : la colonne agrège toujours interro + devoir et + alimente la moyenne. Un libellé déjà personnalisé par une école n'est pas écrasé. + ## [1.0.1] — 2026-09-02 ### Corrigé diff --git a/app/Models/BulletinTemplate.php b/app/Models/BulletinTemplate.php index 76690db..e23241d 100644 --- a/app/Models/BulletinTemplate.php +++ b/app/Models/BulletinTemplate.php @@ -60,7 +60,7 @@ public static function defaultColumns(): array { return [ ['key' => 'subject', 'label' => 'Matières', 'width' => 24, 'type' => 'subject', 'source' => null], - ['key' => 'classe', 'label' => 'Classe', 'width' => 9, 'type' => 'note', 'source' => 'classe'], + ['key' => 'classe', 'label' => 'Devoir', 'width' => 9, 'type' => 'note', 'source' => 'classe'], ['key' => 'compo', 'label' => 'Compo', 'width' => 9, 'type' => 'note', 'source' => 'composition'], ['key' => 'coef', 'label' => 'Coef', 'width' => 6, 'type' => 'coefficient', 'source' => null], ['key' => 'moyenne', 'label' => 'Moy. {periode}', 'width' => 10, 'type' => 'note', 'source' => 'moyenne'], diff --git a/app/Services/BulletinRenderer.php b/app/Services/BulletinRenderer.php index 2946100..6fec45e 100644 --- a/app/Services/BulletinRenderer.php +++ b/app/Services/BulletinRenderer.php @@ -205,7 +205,11 @@ private function noteValue(array $line, ?string $source): string return $this->num($line['by_type'][substr($source, 5)] ?? null); } - return $this->num($line[$source] ?? null); + // La source « composition » (NOTE_SOURCES / colonnes) correspond à la clé + // « compo » du snapshot : sans cet alias la colonne Compo reste toujours vide. + $key = $source === 'composition' ? 'compo' : $source; + + return $this->num($line[$key] ?? null); } private function infoBlock(array $p): string @@ -340,7 +344,10 @@ private function ordinal(int $rank): string private function css(): string { - return <<documents->headerCss() . << prefecture > localite", + "traitement": "noms normalisés en casse de titre ; précisions entre parenthèses retirées ; doubles encodages UTF-8/CP850 réparés ; entrées irrécupérables écartées ; échantillon plafonné à 100 localités par préfecture", + "regions": { + "Centrale": { + "Blitta": [ + "Abalaki Kope", + "Abrougnagni", + "Adjaka Olou Kope", + "Adonvi", + "Afou Djagli Koffi Kope", + "Agbandao", + "Agnankra Yao Kope", + "Agotime Kope", + "Akakpo Deh Kope", + "Akosside", + "Akpani Kope", + "Alao Kope", + "Alfa Kope", + "Amagou", + "Ameyi Kope", + "Andjo Kope", + "Assari Kodjo Kope", + "Assouma Guedeme", + "Atchintse", + "Atoto Kope", + "Awi Kopôçü", + "Ayou Kope", + "Balakpo", + "Bara Soka", + "Bataka Kossi", + "Bernard Kope", + "Bokode", + "Botobossi Kope", + "Boukouli Kope", + "Boyoumodom Kope", + "Dakrokonsou English", + "Debi Kope", + "Diboura Beguedou", + "Dilabo", + "Djato Kope", + "Djipuita Chef", + "Dompe Kope", + "Edomi", + "Emmanuel Kope", + "Etouve Kope", + "Fawiye Kope", + "Fousseni Kope", + "Gaston Kope", + "Gnakouafre Kokou Kope", + "Gomna Kope", + "Guedou Koudjo Kope", + "Hayi Kope", + "Issah Kope", + "Kable Koffi Kope", + "Kamboussa Logba Abalo", + "Katakoui", + "Kate", + "Kiaza-Kope", + "Kodjo Kope", + "Kokonade", + "Komgassi 1", + "Kore Kope", + "Koudzo Kope", + "Koulang", + "Kozolo Kope", + "Kpankpanba", + "Kpawalogologo Kope", + "Kporodji Anango Kope", + "Lakgnan Koffi Kope", + "Lama", + "Lao-Wai", + "Lassa Lom-Nava", + "Leguezim", + "Logba", + "Lougba Kope", + "Malika", + "Martin Kope", + "Mayi Kope", + "Missenou Kope", + "N'danadje Kope", + "N'kinabo Beguedou", + "Nanok Kope", + "Niki-Niki 1", + "Odoh Yao Kope", + "Okouwai Ogrouyo Salifou", + "Pakoute 1", + "Paul Kope", + "Pleza Kope", + "Powali", + "Samafou Kope", + "Sata Kpodji", + "Sika Kope", + "Sokassi Boukpessi Kope", + "Soumdina", + "Takpana Kope", + "Talkate", + "Tassou Kedere Kope", + "Tchanie 1", + "Tchif Kope", + "Tchokossi", + "Tchrami Sidjolo Kope", + "Tendji", + "Togo Free", + "Totame", + "Toule Wai" + ], + "Mô": [ + "Agbamassomou", + "Agrazi", + "Akatou", + "Alabade", + "Assawo-Koura", + "Assomo", + "Atakoura", + "Atika", + "Bato", + "Bessiya", + "Bindila", + "Bolgatanga", + "Boulohou", + "Bounglou", + "Djarkpanga", + "Dogode", + "Folo", + "Folo Abele", + "Folo Banda", + "Folo Kadjala", + "Folo Komna", + "Folo Mangasin", + "Foule", + "Gbanzaba", + "Gnezimde", + "Gnomade", + "Ipouali", + "Ipouali Koutcheti", + "Kagna", + "Kagnigbara", + "Kandjo", + "Koni", + "Kouida", + "Koussimide", + "Kpangam", + "Kpangam Peuhls", + "Kpanti", + "Legede", + "Losso", + "Maladama", + "Mangokoura", + "Matchatom", + "Monpoula", + "Moro", + "Moussoukoudjou", + "N'konta", + "Nabou Bidagbando", + "Nabou Koura Bomkpati", + "Nabou Papaye", + "Nabou Pouryo", + "Nabou Tchatib", + "Okou", + "Passike 1", + "Passike 2", + "Samaoude", + "Sangbanti", + "Sangoul", + "Saïbou", + "Sela 1", + "Sela 2", + "Sibonio", + "Solide", + "Sourkou", + "Sourkou Losso", + "Tabimde 1", + "Tabimde 2", + "Taka", + "Tassi", + "Tchakpisse", + "Tchatchakou", + "Tchatoun -Koura", + "Tchitoua", + "Tchoutchouboura", + "Tindjassi", + "Yaouta" + ], + "Sotouboua": [ + "Abidjande-Kare", + "Adamde", + "Adjade", + "Aflagnon", + "Agombio", + "Agombio Lawai", + "Aleta", + "Alon", + "Aou-Mono Pida Alimbrou", + "Aou-N'gbang", + "Aou-N'gbang Kolinabouaworo 2", + "Aouda", + "Atanko Benin", + "Atanko Lama", + "Atatoude", + "Babade Foulandi Kar0Ubiao", + "Badanabonosi", + "Badaou Mono", + "Baga", + "Betlehem", + "Bodjonde", + "Bouale", + "Boutoum", + "Bovouleme Pouda", + "Bozinabo", + "Carrefour", + "Couvent Des Soeurs De La Providence", + "Dalanda 2", + "Degaulde", + "Dereboua Fatou", + "Dereboua Kpatayou", + "Egbamde Sonde", + "Evaloude", + "Fayen", + "Filandi", + "Fita 2", + "Gnabana", + "Haloukpaboundou", + "Haloukpaboundou Lokinboua", + "Hembela", + "Hezouwe", + "Kaniamboua", + "Kare 2", + "Kassikide", + "Kaza Kpelewaï", + "Kaza Piyowaï", + "Kaza Tchareboua", + "Kazaboua Centre", + "Kazaboua Laoude", + "Kazaboua Pouda", + "Kazaboua Zongo", + "Kazawaï Atokidji", + "Ketchebi Centre", + "Koboyode", + "Kodorade", + "Koming", + "Koming Losso II", + "Konkonde", + "Kouaworou", + "Kouriyo", + "Kpala 2", + "Kpamboure Hilou", + "Kpamboure Sonde", + "Kpamboure Tchololom", + "Kpara-Kpara", + "Kpendjeria", + "Kpete-Kpete -Wuliguede", + "Lagarougou", + "Lama-Were", + "Lama-Were Kpedah", + "Laoude 1", + "Laoude Centre", + "Laoutchoba Issade", + "Laouwai", + "Lawelou", + "Lawelou Piyayo Peuhls", + "Limboua", + "Limbre Losso", + "Lougba", + "Mango Kabre", + "Matchatom Kolide", + "Mazada 1", + "Mewede", + "Mono 1 Bis", + "N'djavezi 1", + "Niamgoulam", + "Nima Agouloude", + "Nima Kelizi", + "Nima Tchalo", + "Panapassa", + "Panlao 2", + "Pikou", + "Pokobana", + "Pouwaï", + "Pouwinepihe", + "Powaï", + "Pya Solimde", + "Sassa-Aouno", + "Sassawai", + "Sessaro Koubende" + ], + "Tchamba": [ + "Abable", + "Abalakata", + "Abalsomou I", + "Abilebou Kope", + "Adjana", + "Adjora", + "Affem Carrefour", + "Affem-Boussou", + "Affem-Boussou Peuhls", + "Afoule 1", + "Agaro1", + "Agbandala", + "Agnimaworo", + "Agoumana", + "Agounaworo 1", + "Akoura", + "Akpotowai", + "Alassane Zozowa", + "Algba Badalo", + "Ali Mango 2", + "Alinde", + "Alolo Peuhls", + "Anakaride", + "Ankoude", + "Armande", + "Assarassi", + "Assoula", + "Atchaigne", + "Azizouwa", + "Bamoede", + "Bigaoude", + "Bilakaza", + "Bloc Alibi", + "Blou 2", + "Bono-Woro", + "Bourkodji", + "Dabade", + "Defale", + "Dikpereworo", + "Djeto", + "Djouledewa", + "Egba", + "Fering Villa", + "Ferme Frontiere Togo Benin", + "Ferme Peuhl 1", + "Ferme Peuhls 1", + "Ferme Peulh", + "Filande", + "Flande", + "Frmou", + "Gagassou 1", + "Ganiou Cope", + "Gnomba", + "Goubi 2", + "Hezoude", + "Kabye Kope", + "Kadjale 2", + "Kala 2", + "Kandji Cope", + "Kapablou", + "Kassakade", + "Keletou", + "Kidjanda", + "Kofelou", + "Koko", + "Kola", + "Koubou Tedjassi", + "Koukoutado", + "Koure I", + "Koutabonou", + "Kpakpaliki", + "Kpanguessi", + "Kpatakpani", + "Kpogolo", + "Lakaba 2", + "Larini", + "Loppo", + "Lotina Lao", + "Mamboulou", + "Mono", + "Mouzou Cope", + "N'djamaka", + "N'koroni", + "N'tchourou", + "Nanwelou", + "Obossou 2", + "Odjomboi II", + "Ognaflou", + "Olale 1", + "Omoudjoa", + "Pagouda", + "Peuhls Nomade", + "Prefet Cope", + "Sama II", + "Semlaou", + "Sere 2", + "Sere 6", + "Sere 9", + "Siarra", + "Solimbia" + ], + "Tchaoudjo": [ + "Abatchang", + "Aboulayi Aledjode", + "Adjimakodo", + "Afadade Nima", + "Agbandaoude", + "Agorode", + "Agoulou", + "Akede", + "Akpakou", + "Aleheride", + "Alfa Mangade", + "Aloube", + "Amaide Nima Bas", + "Amaoude Mo", + "Aou-Mono", + "Ariziki", + "Ataworo", + "Avadade", + "Azanade", + "Baleride", + "Bavile", + "Bdjadade", + "Boboyaya", + "Bonaguana", + "Boubade", + "Bourade", + "Boutonou", + "Bouyo-Lowou", + "Bouzalo Adeli", + "Brinitongnon", + "Dabade 2", + "Dandjane", + "Diguale", + "Djahini Mohamede", + "Djodjoulbia", + "Djoukaride", + "Dougberekoude", + "Dousde Peuhls", + "Efouloulou", + "Farim Kozo 1", + "Ferm Peuhls", + "Ferme Akaba Assih", + "Ferme Moba", + "Ferme Peuhls Tagourgoude", + "Filandi Babayode", + "Filandi Ourode", + "Gaba Soulemane", + "Ganide 2", + "Gborode", + "Goudoude", + "Hezoude", + "Igbeze", + "Iyom 2", + "Kabrede", + "Kadjima", + "Kagnigada", + "Kakanade", + "Kaode", + "Kassena Pont", + "Kedjebi", + "Keguinalime", + "Kerimde", + "Kinabouworo", + "Koboyoworo", + "Kokoro De Atchibodow", + "Kolena-Kaounade", + "Koloware", + "Komire", + "Koudjade", + "Kougbo 2", + "Koungouze 2", + "Kouvon Ouro Dabade", + "Kpakparkpade", + "Kpalafoulassi Bloc", + "Kpangalam Dandjani", + "Kpario", + "Kpaza", + "Kpekpasside", + "Labide", + "Lama-Tessi", + "Lassa Kope", + "Ledyo 2", + "Lombou", + "Losso", + "Lougba 2", + "Molouda", + "Mourawe Peuhl 1", + "N'dimiwe", + "Nigbaoude 1", + "Ouro Djahini", + "Panpankoude", + "Sada Atakpabenon", + "Sada Nima", + "Salimde Filandi", + "Samaoude", + "Sasamde", + "Simdouboua", + "Soboua 3", + "Soboua Tem", + "Solimde" + ] + }, + "Kara": { + "Assoli": [ + "Aboutaria", + "Aboutariya", + "Afidi", + "Agarade", + "Agbakouwaou", + "Agbamsouda Peuhls", + "Agbandaoude", + "Agnakpaode", + "Agnigbem", + "Akoutiya", + "Aledjo-Bas", + "Aledjo-Kadara", + "Aledjo-Kadara Peuls", + "Atchoro", + "Bizenon", + "Bola", + "Bou-Woro", + "Boulade-Centre", + "Boulade-Kabye", + "Boulade-Lamba", + "Boulade-Yaka", + "Bouzire", + "Centre-Arac", + "Dako 3", + "Dako Peuhls 3", + "Dikorida 2", + "Dikoride 1", + "Djahia-Laou", + "Djahia-Peuhls 1", + "Djahia-Peuhls 2", + "Djinde", + "Djinde Peuhl", + "Djondide Peuhl", + "Efolo", + "Egbele", + "Essowazina", + "Gande", + "Gbao", + "Gnata", + "Gninguelia", + "Idingale", + "Kadjaloua", + "Kadjando", + "Kadjimarada", + "Kagnazi", + "Kalampaï", + "Katchalade", + "Kawa-Abonanda", + "Kawa-Abonanda Peuhls 1", + "Kawa-Abonanda Peuhls 2", + "Kayale", + "Kiderou", + "Kigbamleou", + "Kobo", + "Kolo", + "Kouawou-Gbedassi", + "Koudjodoulou", + "Kpalada", + "Kpangatelou Peuhl", + "Kpankpanzi-Bouro", + "Kpayah", + "Kpayah Peuhl", + "Kpayaworo", + "Kpayaworo Peuhl", + "Kpegbela", + "Kpewa", + "Kpindi", + "Lahouyo", + "Lahouyo Peuhl", + "Lama", + "Lao-Woro", + "Lawi", + "Ouro-Gao", + "Peuhls 1", + "Peuhls 2", + "Peuhls 3", + "Peuhls 4", + "Sara-Do", + "Selede", + "Simboua", + "Som-Gaoude", + "Sombohou", + "Sore", + "Soreda", + "Soudou", + "Souri-Kadanga", + "Tagbade 1", + "Tagbade 2", + "Tagbade 3", + "Tagbade 4", + "Tanawaï", + "Tchalimde", + "Tchamda", + "Tchandja-Lao", + "Tchavalim", + "Tchenoude", + "Tchiede", + "Tchirkpe", + "Tchogouma", + "Tomboro" + ], + "Bassar": [ + "Abindjara", + "Adelan", + "Agbassa", + "Aguinda", + "Anima", + "Assowou", + "Atchim", + "Bakoul", + "Bamde Peuhls", + "Batho", + "Bibadjibe", + "Bigrouli", + "Bikoombe", + "Bikpadjabe", + "Binandjoube", + "Binankarb", + "Bissarkpimbe", + "Bitandjor", + "Bitankpare Losso", + "Bitchobebe", + "Bonblido", + "Bosside", + "Boukarde", + "Boulaou Peuhls 1", + "Boundjassi", + "Boungba Peuhl", + "Boutingbedou", + "Dabouti 1", + "Dantchessi", + "Didjoyire", + "Dilabire 1", + "Dimori Bitanim", + "Diwandi", + "Djamblido", + "Dodo", + "Fonaw Peuhl", + "Gmani", + "Gnade Losso", + "Gnigmidou", + "Ighale", + "Istel", + "Kabou", + "Kagnalim", + "Kaliada", + "Kama-Gbandi Peulhs", + "Kamboua-Koudjoukada", + "Karboundi", + "Kassou Peuhls", + "Katcha Losso", + "Katcha-Sampa 1", + "Kawa", + "Kelan", + "Kikarou", + "Kikpang Peuhl", + "Kilawaye", + "Kitangbang", + "Kiwadjoyi 1", + "Kiwan Peuhls", + "Kona Peuhls", + "Koudjobong", + "Koulado", + "Koumboul", + "Koumtoum Peulh", + "Koussatchiou", + "Koutangbaou", + "Kpakpali", + "Kpalako", + "Kpalou-Manga", + "Kpangre", + "Kpantasse", + "Kpatche", + "Labo", + "Lamba", + "Laouda", + "Leleda Peuhl", + "Linatagbanle 2", + "M'bala Losso", + "Makouya", + "Malando", + "Manga", + "Mangado", + "Mele-Mele", + "Monmondo", + "N'boumbyame Losso 2", + "N'fargmando", + "N'gbantammam 2", + "N'kougna-Kassou", + "N'libdo", + "N'nababoun 3", + "Nadjado", + "Nangbani", + "Natchandi", + "Nima", + "Oukpandjatab", + "Outohado", + "Pedjado", + "Pkarataou", + "Samba 2", + "Sanda-Afohou", + "Sansale" + ], + "Binah": [ + "Abribri", + "Adjade-Yoking", + "Adjide", + "Agbarada", + "Alambourgou", + "Alimade", + "Amonde", + "Assanhere", + "Assere", + "Assere-Haut", + "Assere-Plateau", + "Assima", + "Atama", + "Atchakitame", + "Atchao", + "Ayoto", + "Bare", + "Bare-Bas", + "Binano", + "Boa", + "Communaute Peuhls", + "Dambourgou", + "Dihore", + "Doulaka", + "Falade", + "Farende", + "Farende-Pouh", + "Hilimde-Tonah", + "Hilou", + "Hilouda", + "Ihei", + "Kadambara", + "Kadjanga", + "Kadjangade", + "Kadjika", + "Kadjika-Haut", + "Kafaga", + "Kagnikada", + "Kagnissi", + "Karah-Tchabrou", + "Karoh", + "Kasside", + "Katche-Wolete", + "Katodja", + "Kawa", + "Kawoulo", + "Keakou-Bas", + "Keakou-Haut", + "Kemerida", + "Kemerida-Peulh", + "Kenebi", + "Kepobi", + "Keproutchare", + "Ketao", + "Keyakou", + "Kondode", + "Koro", + "Kouamboum", + "Koubaribi", + "Koude", + "Koudjia", + "Kougbe", + "Kougnenetome", + "Kouhobe", + "Kouikpare", + "Koukossile", + "Koukoude", + "Koukoule", + "Koukouware", + "Koulobo", + "Koumongou", + "Koutchani", + "Koutchintchire", + "Kouyala", + "Kouyala-Haut", + "Kouyolo", + "Kouyoria", + "Kpalgou", + "Kpanahore", + "Lama-Dessi 1", + "Lama-Dessi 2", + "Lorou", + "Madjatom", + "Mamande", + "Mantchikide", + "Mayo", + "N'bode", + "N'djada", + "N'djeyi", + "Nandjessi", + "Pagouda", + "Pama", + "Pessare Confess Koulaouda", + "Pessare Confess Kwan", + "Pessare Faro-Koka", + "Pessare Pouh", + "Pessare Tchade", + "Peuhl", + "Pitika", + "Piya-Tokite" + ], + "Dankpen": [ + "Adamoudo", + "Akpalade", + "Assoukpalan", + "Atchore II", + "Balide", + "Bapakdo", + "Batchambo", + "Bidjoma", + "Bikpadjado", + "Bilerdo", + "Binabibado", + "Binangado", + "Biperdo", + "Bitchabodo", + "Bitchido", + "Bombare", + "Borbogou1", + "Bouadjal1", + "Bougabou II", + "Boungbal", + "Bounon Peuhl", + "Bouwedo", + "Dankpentab", + "Dilado", + "Djabado", + "Djabokou", + "Djakondo", + "Djandjambougou", + "Djapoutandou", + "Djatighando", + "Djotawoul", + "Doguimbo", + "Essobodi", + "Fayardo", + "Garkane", + "Gbambokou", + "Gbingbile", + "Gmakido", + "Gmassanpoune", + "Godou", + "Hameau Lamba", + "Iboudjo", + "Igbetab", + "Iwepine", + "Kabogmab III", + "Kadogou", + "Katchamba Centre", + "Keleyir II", + "Kidjaboun", + "Kikpassassine", + "Kinakpateou", + "Kissamonsine-Kpatchalbou", + "Kitoni", + "Koffide", + "Kognido", + "Konol", + "Koudjokponkpon", + "Koukambado", + "Koukpone Carrefour", + "Koulomon", + "Koumer", + "Kouni", + "Koutchan", + "Koutegou II", + "Kpabol", + "Kpal", + "Kpamboua", + "Kpandja", + "Kpapol", + "Kpatalbou", + "Kpatchidi", + "Kpognoundo", + "Kponkpon", + "Lamba", + "Lawta", + "Lidjoblibo", + "Litcherso", + "Lyalibo", + "M'bom", + "Magmele", + "Manehdo", + "Momonde", + "Monkoula", + "N'dilado", + "N'gnobouni", + "N'noughoudou", + "N'sanyare", + "Nabib", + "Nabona", + "Nadoumgbale", + "Nagbidjabo", + "Nagnene", + "Nakpagnodo", + "Namandjol", + "Namon-Centre", + "Nampoia", + "Nandouta", + "Napimbo", + "Nargbal", + "Nassibik" + ], + "Doufelgou": [ + "Adamda", + "Agbande", + "Agbassa", + "Agbassa 1", + "Agbassa 2", + "Agbassa 3", + "Agbassa 5", + "Agbassa 6", + "Agbassa 7", + "Agbassa 8", + "Agnantete", + "Agounde", + "Agounde Centre", + "Akoutanta", + "Akpite", + "Aleonte", + "Alloum", + "Alombre I", + "Alombre II", + "Amaide", + "Amonde", + "Andjide", + "Anima", + "Animade", + "Atchaklao", + "Atchikanta", + "Awanda", + "Awassan", + "Aworo", + "Ayiga", + "Badidigou", + "Bale", + "Bana", + "Bantaragou", + "Bidigou", + "Bidjande", + "Bilgou", + "Birgou", + "Bogaware", + "Bontigah", + "Borga", + "Boulola", + "Bourgou", + "Briga", + "Broukou", + "Dagterga", + "Daoufaga", + "Daoura", + "Delinte", + "Djaganone", + "Djaganone Peuhls", + "Djeregou", + "Djofaga", + "Djorergou", + "Doga-Kawa", + "Doudoura", + "Douga-Eni", + "Fetile", + "Floun", + "Founougou", + "Gagah", + "Galga", + "Hagou", + "Hirno", + "Houladougou", + "Hounahou", + "Hounde", + "Hourkpame", + "Houseremon", + "Kadjalim", + "Kadjalla", + "Kadjere", + "Kamante", + "Kantamboli", + "Kaouno", + "Kawa-Bas", + "Kawa-Haut", + "Kawaga", + "Kawanga", + "Kawoukou", + "Kawouno", + "Kirga", + "Konfaga", + "Kontenga", + "Kore", + "Koudougoua", + "Koukou", + "Koulinte", + "Kounadjere", + "Kounde", + "Kounfirgou", + "Koutakou", + "Koutanapiwiya", + "Kouwere", + "Koïnte", + "Kpabte", + "Kpade", + "Kpadebe", + "Kpadiga", + "Kpahire" + ], + "Kozah": [ + "Abide", + "Aboïda", + "Adjaada II", + "Adomde Peuhl1", + "Agbalossi", + "Agbanda", + "Agbande-Bas", + "Agbandi", + "Agnade", + "Agnassimde", + "Ahodo", + "Alloum", + "Anlommou", + "Atcheleda", + "Awandjalo", + "Babarde", + "Bodoouda", + "Bonoda", + "Bouda", + "Bounoh", + "Dealoou", + "Djamde", + "Elimde", + "Fehing", + "Ferme Peuhl", + "Flandi-Kagbanda Mausolee", + "Gnakoude", + "Gnaoude", + "Gnigbanda", + "Gningbang Hade", + "Haltewou", + "Houde", + "Kaade", + "Kadjada", + "Kadjiyo", + "Kandalao", + "Kandjide", + "Kassi", + "Katakpa", + "Kayade", + "Kessan Tchanagade", + "Kodah", + "Kolade", + "Kongozida", + "Koudjoukada", + "Koulounde", + "Kpalassi", + "Kpamwaï", + "Kpanguiye", + "Kpatayou", + "Kpawouyo", + "Kpeloude", + "Kpeta I", + "Kpindi", + "Kpindi Peuhl2", + "Kpiwade Peuhl", + "Lama-Bohou", + "Lama-Kolide", + "Lambo", + "Landa", + "Lao", + "Lao-Haut", + "Lassa-Lao", + "Lidjonda", + "Makoude", + "Mandela-Haut", + "Melmessede", + "N'tchoou", + "Nandade", + "Peuhl Gnagnade", + "Pidah", + "Pitah", + "Pittah-Poudah", + "Piyayo", + "Piyou-Haut", + "Pou-Haut", + "Poulou", + "Powaï", + "Pya Hodo Awidina", + "Samala", + "Samala-Bas", + "Saoude", + "Sedina", + "Sedina-Peuhl", + "Setide", + "Siouda", + "Sodoa", + "Sonde", + "Tchaada", + "Tchamde", + "Tchaoude", + "Tchedide", + "Tcheou", + "Tchila Peuhl", + "Tchoide", + "Tcholla", + "Tcholou", + "Tchoou", + "Toblao", + "Toumboua" + ], + "Kéran": [ + "Abenta", + "Adjaïte", + "Aflemata", + "Anima", + "Ankaliyo", + "Assouta", + "Atchifnore", + "Atchikata", + "Atikpeta", + "Awanda", + "Ayanta", + "Dapien", + "Dissani", + "Finkata", + "Gnarouwere", + "Gnarouwere 2", + "Hawonlada", + "Houde Lao", + "Houkouyote", + "Houlio", + "Houloure", + "Housermon", + "Ibotan", + "Kamaka", + "Kankanboulio Peuhl Quartier P", + "Kankankpe", + "Kara Port", + "Koble", + "Kofi Ferme", + "Kokou Tamberma", + "Konte", + "Kontewe", + "Koubieti", + "Koufantouagou", + "Koukotchiengou", + "Koukotougou", + "Koukpanougou", + "Koulangou", + "Koumakou", + "Koumbi I", + "Koumer", + "Kounanoungou", + "Kounassikou", + "Kouniti", + "Kounkounkpe 1", + "Koupinda", + "Koussakpangou", + "Koussogou", + "Koutago", + "Koutamagou", + "Koutando", + "Koutapa", + "Koutchagou", + "Koutessogou", + "Koutougou", + "Kouwangou Ferme", + "Kouyakougou", + "Kpamanta", + "Kpamperta", + "Kpande Yacoub", + "Kpaouta", + "Kpaskpaï Ferme", + "Kpelinga", + "Kpoyade", + "Labore", + "Layota", + "Lekounwo", + "Libouli", + "Lina", + "Lipouli II", + "Liteni", + "Mafate", + "Maliham", + "Mandoute", + "Manta Ferme", + "Matoti", + "Mossota", + "Mouloum 2", + "N'konta", + "Naboudjoum", + "Nambou", + "Namoute", + "Nande", + "Nantagou", + "Napo II", + "Natchangou", + "Nioucira", + "Omalo", + "Ossacre Nouveau", + "Oukouyo Namale", + "Oursemon", + "Palapre", + "Pampikou", + "Pangouda", + "Quartier Epp", + "Santougou", + "Sibouri", + "Simbine 2", + "Sintewara", + "Sintwota" + ] + }, + "Maritime": { + "Agoè-Nyivé": [ + "Adeti Kope", + "Adoglove", + "Afiadenyigba", + "Agbaleti Kope", + "Agbegblognam", + "Agbekovi", + "Agbleliko", + "Agnigbawou", + "Agnigbe", + "Agossito", + "Ahaligan", + "Ahonkpoe", + "Akissi Kope", + "Aklassou Kope", + "Akoin", + "Akpama-Kadja Kope", + "Alinka", + "Alinka Nyivemegble", + "Alinka-Gato Kope", + "Alinka-Kome", + "Alinka-Zogbedji", + "Alodji Kope", + "Amadenta Anagli Kope", + "Amavi", + "Amedenta Aki Kope", + "Amedenta Kleve", + "Amedome", + "Amla", + "Anyave", + "Assiko", + "Athieme", + "Athieme- Anonkpoe", + "Atigan Kope", + "Atri Kome", + "Atsanve", + "Attie Kpodzi", + "Attitin Gome", + "Avinato", + "Awoudi Kome", + "Azougo", + "Biam Kope", + "Bobi Kome", + "Bokpoko", + "Daliko", + "Dalime", + "Dangbessito", + "Darakpodzi", + "Degome", + "Deklou", + "Dekpor", + "Devime", + "Dikame", + "Djadou Kope", + "Djemeki Kope", + "Djinekou", + "Djove", + "Dodzi", + "Dopegno", + "Ekpo Kope", + "Ewou", + "Ezion Kope", + "Fidokpui", + "Gbologan Kome", + "Gonyi Kope", + "Guenou Kope", + "Guinnou", + "Hewou", + "Karevi", + "Kleme", + "Klikame", + "Kohe", + "Komahe", + "Kombe", + "Kopegan", + "Kouwonou", + "Kove Ahondji Kope", + "Kove Sivagnon Kope", + "Kpedevi Kope", + "Kpokplovime", + "Kpokpome", + "Kpotave", + "Kpoti Kope", + "Lalle Kope", + "Late Kope", + "Legbassito", + "Logoe", + "Lomegno Kope", + "M.s.a", + "Madjikpeto", + "Mensanvi Kope", + "Mokli Kope", + "Nanegbe-Zoussime", + "Rama", + "Sabath", + "Sanvi Kondji", + "Sanyrako", + "Silivi Kope Ou Adidome", + "Sokoukome", + "Sowou Kope", + "Taga Kope" + ], + "Avé": [ + "Abi Kope", + "Abolebevi", + "Adenyo Kope", + "Adjfi Kope", + "Adzekpe Kope", + "Afanoukoe Kope", + "Agbalaho", + "Agbessia", + "Agbo Kope", + "Agninou", + "Agogome", + "Agotime Takpla", + "Agoutevi Kope", + "Ahoto Kope", + "Akondji", + "Akpatro Lire Akpatchro", + "Akutsazve", + "Alassani Kope", + "Amegadze Kope", + "Amilassime", + "Ando Centre", + "Ando-Bedo", + "Anyron", + "Assiamagble", + "Atsakpo", + "Atti Apedokoe", + "Atti-Touwui", + "Attiogbe Kope", + "Avedzeta", + "Avenon Ou Afagbedzi Kope", + "Avou Kope", + "Azi Kope", + "Azouma Kope", + "Bagbe Kpedji", + "Baka Kope", + "Batoume", + "Bedzeme", + "Betenve Ou Agodome", + "Boga Kope", + "Bosro Kope", + "Dadji Kope", + "Dangbui Kope", + "Degame", + "Devie Kope", + "Djanta Kope Ou Alain Kope", + "Dodzi", + "Donyo Kope", + "Dridrive", + "Dzogadze", + "Dzoka Kope", + "Dzreve Avodite Kope", + "Edzi", + "Eklou Kope", + "Emimo Kope", + "Fedeme", + "Fialalonou", + "Fokpo", + "Gabi", + "Gakpodji Kope", + "Gbessa Kope", + "Goeh Kope", + "Habia Kope", + "Hekpe", + "Ho Kope", + "Houno Kope", + "Kablevi Kope", + "Kassa Kope", + "Keke", + "Klobave", + "Kodjo Ave Kope", + "Koho Kope", + "Konta", + "Koudassi Gare", + "Koyibo", + "Kpedja", + "Kpetsu Gome", + "Kpodzi Centre", + "Kudragbe Donomade", + "Loome", + "Makoumavo Ahiangbe Kope", + "Man Togo Trois Fermes", + "Mekoviade", + "Nassogne", + "Noukpo Kope", + "Nyitakpo", + "Raymond Ou Kodjovia Kope", + "Sanvi Kope", + "Segnon Kope", + "Singli", + "Somenou Kope", + "Tamanyo Kossi Kope", + "Tedahoe", + "Todome", + "Toveme Ou Agbedo Kope", + "Tsiviepe", + "Veko", + "Wokpla -Ekpui", + "Wonougba", + "Yokoutikpota", + "Yovo" + ], + "Bas-Mono": [ + "Abalo Kope", + "Adagboe", + "Adeho-Toulassi", + "Adje Yaovi Kope", + "Adote Kope", + "Afaledji Kope", + "Afantchao Kope", + "Agbadessikou Kope", + "Agbedouve", + "Agbetiko", + "Agbleta-Mawoussi", + "Agbokope", + "Agbozome", + "Agome-Glozou", + "Agouna Kope", + "Ahadji Lope", + "Ahonlogoe", + "Akakpo-Djakpata Kondji", + "Aklobeshi Kope", + "Akouesso Kondji", + "Akpan Kope", + "Alabi Kope", + "Alidje Kope", + "Aloesso Kope", + "Alouenou-Kpeteto", + "Amededjisso Kope", + "Amegnikpo Kope", + "Ametogan Akpui Kondji", + "Amouzou Michel Kope", + "Anassigble Kope", + "Assagba Kope", + "Assilamenou Kope", + "Atche Kope", + "Atsu Kondji", + "Attitogon", + "Avoutokpa", + "Ayaovi Kope", + "Aziaba Kope", + "Batonou", + "Bleble Kope", + "Broda Kome", + "Dansou Peme", + "Dazouhouen Egou Kope", + "Deti Kope", + "Djakpassou Kope", + "Djigbe", + "Dogbowia Kope", + "Dossa Kope", + "Dotse Kope", + "Dzogbedji", + "Eklou Kope", + "Fiessinou Kope", + "Follykoe Kope", + "Gaffa Peme", + "Gbandi Latehoe", + "Gbedova Kope", + "Gbogbo Kope", + "Gnagblodjo Kope", + "Gnonkou- Kope", + "Hagbe Kope", + "Hogble Peme", + "Hontowou", + "Houndebakin Kope", + "Jacob Kope", + "Katassi Kope", + "Klatome", + "Klobessi Kope", + "Kofigate Kope", + "Kossi Kondji", + "Koudahe Kope", + "Kouglo Kope", + "Kouwonou Kope", + "Kpadonou Kope", + "Kpodjodo Kope", + "Kpoteme", + "Logossa Kope", + "Logowome", + "Louis Kondji", + "Mavouime", + "Messan Akpo Kope", + "Midresso Kondji", + "Mome-Gbave", + "N'titidome", + "Novidjro Kope", + "Okansenou Kope", + "Raphahel Zogbegan", + "Sarakawa", + "Sekpona", + "Silete Kope", + "Soledji Kope", + "Somabe Kope", + "Souza Kope", + "Tannou Kpota", + "Titega Kope", + "Togblo", + "Tognon Kope", + "Tonato Kope", + "Tota", + "Toukpo Kope", + "Vessome" + ], + "Golfe": [ + "Agba", + "Akakpo Kope", + "Akato Avoeme", + "Akato Deme", + "Akato Viepe", + "Apedokoe Gbomame", + "Awou Kope", + "Dekpor", + "Klegome", + "Kleme Agokpanou", + "Kleme Kpodzi", + "Kleme Yewepe", + "Lankouvi Sakani", + "Segbe Douane", + "Segbe Kpodzi", + "Segbegan", + "Tali Kope", + "Wougome", + "Zanvi" + ], + "Lacs": [ + "Abalo", + "Abla Kope", + "Adate", + "Adjavoin Kondji", + "Adjivoin Kondji", + "Adjove Avessou Kondji", + "Adjove Koutepe Kondji", + "Adouto Kope", + "Afanou Gagnon Sena Home", + "Afidegnigban", + "Agbassou Kondji 1", + "Agbegbo", + "Agbevide Knodji", + "Agbodjinou Kondji", + "Agbogbon Kondji", + "Agbowoanou Kope", + "Agome Seva II", + "Ahadji Kondji", + "Akedjo", + "Akoete Kpegan", + "Akouvi Kondji", + "Alemou Kondji", + "Amah Kondji", + "Amedikou Kope", + "Amelessessi Kope", + "Ametowoyona Kondji", + "Anagonou Koklokponou", + "Anani-Ameta", + "Anoumouvigbe Kangni Kondji", + "Apeyeme Ou Mensavi Doukou", + "Assou Kope", + "Atchan Kope", + "Attiogbe Kondji", + "Aveve", + "Awoudja Kope Ou Agbavon", + "Ayewossona Kope", + "Azanta Kope", + "Azimegan", + "Batame Outete Kondji", + "Boko Kope", + "Dansou Kope", + "Degbevi Kondji", + "Djakpata Kondji", + "Djifa-Kpota", + "Djokoto Kondji", + "Dossa Kondji", + "Douanier Kope", + "Egoh Kondji", + "Enyowou Biova", + "Fiata Djigbe", + "Folly-Kope", + "Gabi Kope", + "Galevi Kope", + "Gardien Kope", + "Gbekoukoui Kondji", + "Glidji", + "Goudjovi", + "Hetcheli Kondji", + "Honou Kope", + "Hounkpati Kondji", + "Kako Kope", + "Ketosou Kondji", + "Kloutse Kondji", + "Koevi Attivi Kope", + "Kofivia Kondji", + "Koleafo Klougakenou", + "Koro Kope", + "Koudo Kondji", + "Koumondji Kope", + "Koutigbe", + "Kpanoutche Kope", + "Kpodjro Kope", + "Kponko Kope", + "Kpota Haoussakome", + "Kuevi Kondji", + "Latevi Kope", + "Lossovi Kondji", + "Mederos Kondji", + "Mensan Laga Kondji", + "Missiagbeto Kope", + "Ndiameko Kope", + "Nouvedon Kondji", + "Nvlle Construction Agome Seva II", + "Peda Kondji", + "Sagbo Kondji", + "Santana Ou Houdouwadan Kondji", + "Sedjro Kondji", + "Semeglo Ou Kadonya Kope", + "Sessou Adjisse Kondji", + "Sewatsrikope -Kpeme", + "Sipoaka Kope", + "Sodja Kope", + "Sossou Amouvi Kope", + "Tango Kope", + "Tela Ou Ahonon Kope", + "Tigoe Kope", + "Togneviadji", + "Tokpogan", + "Tonon Kondji", + "Tota" + ], + "Vo": [ + "Abalo Kope", + "Adan Kope", + "Adidovi Amedokpo Kope", + "Adjete Kope", + "Afadjinou Kope", + "Afowime", + "Agbeke Kope", + "Agbeve Houzouke Kope", + "Agbodja Kopegan", + "Agbovi Kpota", + "Agokponou", + "Agoutio-Ko", + "Akakpovi Aklama Kope", + "Akoessovi Kope", + "Alade Kope Ou Sewa Kope", + "Alowou Peme", + "Amegbo Kope", + "Amevikope", + "Anagodo Peme", + "Animabio", + "Apegbo", + "Apeyeyeme", + "Assiwloinvi", + "Atchandome", + "Atogo Peme", + "Atsansi Todome", + "Attikple Akesso Kope", + "Avegodo", + "Ayaovi Sagbla Kope", + "Ayivi Dalolo", + "Bame", + "Bessan Deamessi Kope", + "Boko Todome Koussin 2", + "Dablavi Kope", + "Dapenou Kouradze Kope", + "Djagli Kope", + "Djimesse Kope", + "Djokoto", + "Donou Peme", + "Dovi Kope-Agovi", + "Dzrekpo Centre", + "Ekpe Peme", + "Fatodji Kope", + "Fovi Kope", + "Gamele Kope", + "Gbogame", + "Glope Segbenyo Koudji", + "Gota Home", + "Hangbakoe", + "Hoinsso Kope", + "Houdjago Kope", + "Hounkpe Kope", + "Jeanvi Kope", + "Kenou", + "Kletsi Guidi", + "Kodjo Kope", + "Koffi Gnemevo Kope", + "Koleguin Kope", + "Konkevi", + "Kossi Teko Kope", + "Kouenou", + "Kounouhopeme", + "Kouwonou Kope", + "Kpedja Kope", + "Kpetou Gouwo Honou", + "Kpokpo Koffi Kope", + "Kpota Gbanto Kope", + "Lanve Kope", + "Live Klo Ou Yakpe Kope", + "Lomnava", + "Mamissi Kopegan", + "Mekponoukope", + "Midahoundji Kope", + "Minogblon Kope", + "Mome Balime", + "Nameli Anani Kope", + "Noukou Sodolo Kondji", + "Nyitame", + "Paulin Kope", + "Sadali Kope", + "Sape Gota Honou", + "Semebio Kope", + "Siakaro Tchagome", + "Sodoli Bokon Kope", + "Sokladji", + "Sokome 2", + "Solly Kouwonou Kope", + "Sotole Kope", + "Sunday Kope", + "Tchideme", + "Teteyi", + "Togbe Anatchi Kope", + "Tohona Kope", + "Tome Kope Gatidji", + "Toukoui Kope", + "Tsiko Honou", + "Vo-Aso", + "Wlame", + "Yaovi Agbodja Kope", + "Zevidjemonou 1" + ], + "Yoto": [ + "Abalo Kondji", + "Adabadi Kondji", + "Adankpo Kope", + "Adjaho", + "Adjavon Kope", + "Ado Kope", + "Adossi Kondji", + "Afandi Kope", + "Afanoukapita", + "Afidegnon Kope", + "Afougandji Kope", + "Agbano Kope", + "Agbessinou Kope", + "Agboguede", + "Agoh Kope", + "Agoto Kope", + "Ahepe Assikor", + "Ahokponou Kondji", + "Akitikpo Kope", + "Aklou Kope", + "Aladji Kope", + "Alokpadji", + "Amedjrovi Kondji", + "Amelagbessi Kope", + "Amou Kope", + "Anagonou Kondji", + "Anlongan-Kotobada", + "Apemekou Kope", + "Assogba Peme", + "Atakpo Kope", + "Atogou Kope", + "Atse Kope", + "Atsuhoe", + "Attive", + "Avekpatse Kope", + "Awesso Kope", + "Awoussi Kondji", + "Ayena Peme", + "Aziamate Kondji", + "Babatoe", + "Balovi Kope", + "Benoit Kope", + "Bognon Kondji", + "Dadjehoe", + "Davon Kope", + "Dekandji", + "Didjan Kondji", + "Djerome", + "Djo Kondji", + "Dodonou Kope", + "Dokpo Kope", + "Dosseh Kope", + "Dzerekouba", + "Edri Kope", + "Ekoe Kope", + "Esse-Gble", + "Fia Kope", + "Gabadjin", + "Gaoussi Kondji", + "Gbagba Kondji", + "Gbeve Kope", + "Gboto-Kossidame", + "Gligue Kope", + "Godjinme", + "Gougoume Centre", + "Guimetena Kope", + "Hessou Kope", + "Honouho Cope", + "Houngbeke Kondji Ou Seva", + "Hounlessodji Kope", + "Hounyowou Kpota", + "Kedegnohoe", + "Kloyo Kondji", + "Koevi Kope", + "Kokouvi Peme", + "Komlanvi Kope", + "Kossihoe", + "Koudjoakou", + "Kounou Kondji", + "Kovito", + "Kpeklo Kope", + "Kpetotsi Kope", + "Kpokpo Kondji", + "Krague Kope", + "Lavagnon Kope", + "Logo Kondji", + "Lotou", + "Mangotigome", + "Messan", + "Mignanou Kope", + "N'bouke Kondji", + "Nassoudehoe", + "Noutonkou Kondji", + "Paulvi Kope", + "Sedome", + "Sepenou Kondji", + "Sikpe Kpota 1", + "Sodjagbo Adjalle Kope", + "Soglohoun Kope", + "Sonli Kope" + ], + "Zio": [ + "Abali Kope", + "Abotsi Kope", + "Adidome", + "Adjeoda Dabla Kope", + "Adoko", + "Afaizo Kope", + "Afoudome", + "Agbassa Koami Kope", + "Agbenossi Kope", + "Agbodjekpo", + "Agobaya Kope", + "Agossou Kope", + "Ahato Kopegan Ou Togbui Aziabo", + "Akadjame", + "Akoedji-Kope", + "Akpaka Sogbo Ou Zoli Kope", + "Alagnon Kope", + "Amavevi", + "Amenoukope", + "Amouzou", + "Anyidzi", + "Assigame", + "Atchanve", + "Atisso Kope Ou Kloudji Kope 2", + "Atsave", + "Attikpo Kope 2", + "Avelebe", + "Awoude Kope", + "Ayawavi Glissi Kope", + "Aziakou Ozouame Kope", + "Azoule Kope Ou Agbodan Kope Netime", + "Bazo Kope", + "Boccovi Kope", + "Boutome Kope", + "Dafolegname", + "Danyo-Kope", + "Dedje", + "Dick Kope 2", + "Djidokpo", + "Doate Kope", + "Dougna Kope", + "Dzatikpo", + "Efoute Kope", + "Encadreur Tagba Kope", + "Ferme Akicole Svd Societe Des Verbes D", + "Fofovi Kope", + "Fraganduia", + "Game Agokpe", + "Gape Egbe", + "Gavo Kope 2", + "Gbessa Kope", + "Glenou", + "Gomado Kope", + "Hakedji", + "Hegnonpeme Ou Gake Kope", + "Hooko Kope", + "Hounyome", + "Kable Kodzo Kope", + "Kanyi Kope", + "Ketapui Agbledji", + "Klouvi Kope", + "Kodjo Kouma Kope Ou Chebi Kope", + "Koffi Houssi Kope", + "Koklovi Kope", + "Kokouvi Toviaho Kope", + "Komlan Ako Anani Kope", + "Kossi Kope1", + "Koudike Kope", + "Kouma Ago Kope", + "Kouvionou Kope", + "Kpe Kope", + "Kpetsi Kope", + "Kpokou Karevi Kope", + "Laba Kope Ou Komlangble Kope", + "Lebe II", + "Lokpa Kope", + "Madja Kope", + "Mawoule Kope", + "Messeko Kope", + "Moba Kokou Kope", + "Monto Kope", + "Nks'ntole Kope", + "Nouwozan Kope", + "Otounde Kope", + "Pepekale Kope", + "Rigobert Kope", + "Santana Kope", + "Semeglo Kope", + "Sodokpon", + "Sorofia Kope", + "Taga Kopevi", + "Teatcher Kope", + "Todjro Ou Djoke Kope", + "Tokpli", + "Tovon Kope", + "Tsikale", + "Vougodo", + "Wonegnon Kope", + "Yao Douvon Kope", + "Zadzi Kope" + ] + }, + "Plateaux": { + "Agou": [ + "Abalo Kope", + "Abotsi Kokou Kope", + "Adebleou Kope", + "Adjete Kope", + "Adzake Kope", + "Agadome", + "Agbegnigan Kope", + "Agblodome", + "Agbodza Kope", + "Agoevi", + "Agotime Wodome", + "Agoudouvou", + "Ahogbe Kope", + "Akayi Kope", + "Akpabla Kope", + "Aladji Kope", + "Alo Kope", + "Amangome", + "Ameganvi Kope", + "Anyative", + "Assali Koffi Kope", + "Ataklo Yao Kope", + "Atitcho Kope", + "Aveglo 1", + "Aveve Kope", + "Awoudi Kope 4", + "Aziabou Kope", + "Babanra Kope", + "Bakoma Kope", + "Batotodzi 2", + "Bethania", + "Bludo Kope", + "Canan", + "Dafo Kope", + "Dawlessou", + "Devin Kope", + "Djamdja Kope", + "Djogbe", + "Dongo Kope 1", + "Dzigbe", + "Edza Kope", + "Egbo Kope B", + "Eko Kope", + "Emoin Kossi Kope", + "Fabric 1", + "Fiagbome", + "Gadza Woukpe", + "Galle Kope2", + "Gbatedo Kope", + "Gbogatse Kope", + "Godja Kope", + "Gumedzo Yao Kope", + "Have", + "Hoto Kope", + "Kable Kondji", + "Kagne Kope II", + "Kata Kope", + "Katoe Kope", + "Keleou Georges Kope", + "Klounou", + "Kodjovi Kope", + "Kokouma Kope", + "Kolou Kope", + "Koto Kope", + "Kozah Kope II", + "Kpatoe Kope", + "Kpepoume Kope", + "Kpodjahon", + "Kpovenou", + "Lama Kope", + "Lawoe Kope", + "Lolonyo Kope", + "Magnon Kope", + "Mama Kope", + "Matias Kope 2", + "Messankpui Kope", + "N'gnama Kope", + "Neti Kope 3", + "Nomessi Kope", + "Nyamessi", + "Opana Kope", + "Paul Kope 2", + "President Kope 1", + "Sami Kope", + "Segbedji Kope", + "Sister Kope", + "Sossi Kope Ou Dolagbenou", + "Tagbolo Kope", + "Tapissie Kope", + "Tawila Kope", + "Tchihe Kope", + "Todzro Kope", + "Toudzi Kope", + "Tsave Kope", + "Vamatekpo 1", + "Victor Kope 1", + "Vounou Kope IV", + "Wogboe", + "Wutegble", + "Yovogan Yao Kope" + ], + "Akébou": [ + "Adankpe Akebou", + "Adoudepe", + "Aferique", + "Afiadegnigban", + "Afomedi", + "Agbelive", + "Agbelive Todji", + "Agbetri", + "Aka", + "Aka Kope", + "Akouati", + "Akpene Kope", + "Alowou Kope", + "Amedjope", + "Amouka", + "Aneho", + "Apekou Kope", + "Arima", + "Atchakagbene", + "Atekpo", + "Atideka", + "Atigoza", + "Atsete", + "Ayagba", + "Baguida", + "Banse Ewui", + "Bato", + "Bekro", + "Bonglo", + "Boussey Kope", + "Brada", + "Brounfou", + "Counte Cope", + "Dagnigan", + "Dagnivi", + "Dahon Kope", + "Dakan", + "Densou", + "Djakpodji", + "Djidjokou Ewui", + "Djimambo", + "Djitriame", + "Djiuire", + "Djon", + "Djon Atigoza", + "Djon Brukuku", + "Djon Danye", + "Djon Elavagnon", + "Djon Gape", + "Djon Kpando", + "Djon Tessani", + "Dorolakpanou", + "Dougan", + "Dougan Ferme", + "Doukomele", + "Douyoure", + "Elavagnon", + "Elavagnon II", + "Esseleke", + "Essine Kope", + "Fon", + "Fon Todji 1", + "Fon Todji 2", + "Foto Hoho", + "Foto Yeye", + "Fotowossi Ewui", + "Gbende", + "Gbognikre", + "Gnande", + "Gnimoinro", + "Gnoske Kope", + "Goudeve Yeye", + "Guezere Kope", + "Hohoe", + "K'senke", + "Kabagni", + "Kable Kope", + "Kabo", + "Kamalo", + "Kanko", + "Karabani", + "Kete Kope", + "Kodjo Aza", + "Koffi Ewui", + "Kolatiako I", + "Kolatiako II", + "Kolatiako III", + "Konitse Ewui", + "Konle", + "Konsoua I", + "Konsoua II", + "Konsoua III", + "Konsoua IV", + "Korou", + "Kotora", + "Kougnonhou", + "Kpalave Gbohoho", + "Kpalave Gboyeye", + "Kpalave Kope", + "Kpeve" + ], + "Amou": [ + "Abassa Kope", + "Adape", + "Adiva", + "Adjahoun-Todji 1", + "Adogli Agbadomondji", + "Afidegnigban", + "Agadji Todji 1", + "Agbangba", + "Agbodomondji", + "Agbozoume", + "Agomaza", + "Akakpe", + "Alafo", + "Ali Kope", + "Amlame", + "Amoussa Kope", + "Ananivi Kope", + "Assakpa", + "Assogba Kope", + "Avedje", + "Azafi", + "Bago Kope", + "Bassavi Kope", + "Basse Gnamessi", + "Bato Goune", + "Bouto", + "Demandeli", + "Didokpo", + "Djalouma", + "Djatowi", + "Dossou Kope", + "Doumessi Kope", + "Ebeva", + "Edoko", + "Ekpegno Ognawlou", + "Ekpeta", + "Enagbedje", + "Eviawou", + "Evou Bethel", + "Ezime", + "Gbegbe", + "Gbobe", + "Gnamassile", + "Goudeve", + "Guezere Kope", + "Ibefo", + "Idifiou", + "Igbowou Amou", + "Illico Zebe", + "Itchekou", + "Iwassi", + "Iwoto", + "Kadoasso Kope", + "Kante Kope", + "Kassou Kope", + "Kawa", + "Kobi", + "Kolam", + "Koudjoufayi", + "Koutoukpa", + "Kpategan", + "Kpete", + "Kpoda", + "Kpokpo", + "Lare Kope", + "Mava", + "Mayaba Kope", + "Mouna", + "Nakpabo1", + "Nyilegan Kope", + "Odokou", + "Ogobe 1", + "Okahou", + "Okpakou", + "Olelou", + "Omouva", + "Otadi", + "Ouga", + "Oukle", + "Oukpo-Aza", + "Oulita Plateau", + "Ouvetsebe", + "Ouyouwou", + "Patatoukou", + "Petro Kope", + "Sagouda", + "Sevia", + "Sodo 2", + "Sodo-Zongo", + "Soukayile Kope", + "Tchaa Kope", + "Tchale Kope", + "Tchenekewlou", + "Tebelao", + "Teme-Oulo", + "Togble", + "Toumon", + "Vogan Kope", + "Yaboui", + "Yalla" + ], + "Anié": [ + "Abadjene Kope", + "Abekouta", + "Adam Kope", + "Adi Kope", + "Adjagnon Kope", + "Adjassika 1", + "Adjigo", + "Adjo Kope", + "Adolphe Kope", + "Adonou Kope", + "Afole", + "Agbakotan", + "Agbandaou 2", + "Agbole", + "Agname", + "Agodjololo", + "Agossou", + "Agouna Deve", + "Ahoeyeye", + "Ahounou", + "Akaba Plateau", + "Akoda Kope", + "Akpaka", + "Akplave", + "Akri Kope", + "Aladja Kope 1", + "Alagbe", + "Aloha", + "Aloum", + "Amou Kope", + "Anagbanme Kope", + "Arife", + "Aseka", + "Assika Kope", + "Assogba Kope", + "Atake", + "Atchinedji", + "Atewe 2", + "Avevoin", + "Avou Kope", + "Ayebou Kope", + "Azonaha", + "Bantchi Koura", + "Bayaho", + "Bayedje 2", + "Blaisetohoe", + "Bourkina", + "Carrefour", + "Dagni Kope", + "Datcha Kope", + "Degla Kope", + "Deux-Cent Vingt", + "Djintohuin Adeli", + "Djokobo Kope", + "Djougou Kope", + "Dogo Kope", + "Dokpe", + "Dosse Kope", + "Dote", + "Efoufami", + "Etogbegna", + "Ezao Kope", + "Fadegnon Kope", + "Flema", + "Fontedji Kope", + "Founfounfere", + "Gassin Kope", + "Gbagbadjakou 2", + "Gbedoukpo", + "Gbodjindjin-Sada", + "Glito", + "Gnani Kope", + "Godobo", + "Gourma-1", + "Gueloufo Kope", + "Hilair Kope", + "Honou", + "Houndje", + "Hounto", + "Ido Kope", + "Ile Bansa", + "Ile Kolor", + "Ile Yiboe", + "Ilekohan", + "Iroukoura Kope", + "Jerusalem", + "Kabye Kope", + "Kako Kope", + "Kakou", + "Kanabia", + "Kanikani", + "Kaoura Kondo Kope", + "Kataka Kope", + "Kodjekan", + "Kokou Kope", + "Kolo Kolo Kope", + "Komla Kope", + "Konigbo", + "Kotchi", + "Koufohou 1" + ], + "Danyi": [ + "Abalofio", + "Adogliho", + "Afantchao Kope", + "Afiadenyigba", + "Afougbadze", + "Agame", + "Agoumatsa - Todzi", + "Ahloeve", + "Akayo Ferme 1", + "Akayo Ferme 2", + "Akayo I", + "Akayo II", + "Akpaliwoe", + "Akpaliwoe Hameau", + "Ananas Kope", + "Ankuvi", + "Anyigbe", + "Assassehini", + "Assimelogbe", + "Atakple", + "Atali Kope", + "Atigba", + "Atila-Koussa", + "Awounadjassi", + "Babahoe", + "Bafa", + "Bamalo Kope", + "Batoume", + "Blitsa Togoe", + "Bogo", + "Dafo", + "Danyi - Amegape", + "Danyi Kakpa", + "Danyi Nyamegbe", + "Danyi Tchadome", + "Danyime", + "Denou", + "Deve", + "Dzedrame", + "Dzedramegan", + "Dzogbegan", + "Dzokly", + "Evime", + "Eza Kope", + "Fufu", + "Gabi", + "Gaitodji", + "Gbaladje", + "Hiheatro", + "Houto", + "Iba", + "Ito", + "Itobole", + "Kekeoude", + "Keteme", + "Keteme Tonome", + "Konda", + "Kossahoe", + "Koudzragan", + "Koudzravi", + "Kpeto", + "Kpeve", + "Kponoe", + "Kussuntu", + "Leloume", + "Loutoe", + "Magaman Kope", + "Medewunui", + "Mempeassem", + "N'digbe", + "Nondowou Kope", + "Nyamegbe-Wutou", + "Nyanya - Aledide", + "Nyigbeme", + "Nyuimabou", + "Oroti", + "Ounago", + "Sassanou", + "Setivi Kope", + "Tchamdjade", + "Tekpanadji", + "Tetebe", + "Tinipe", + "Toglo", + "Tokoe", + "Tokpo", + "Tonome", + "Tonotse", + "Tovi", + "Tsadome - Tonome", + "Tsavanya", + "Wetrofe", + "Woeve", + "Woluko", + "Yikpa - Djigbe" + ], + "Est-Mono": [ + "Aba 1", + "Abinga Kope", + "Adam Kope", + "Adjakpekpe Kope 1", + "Adjoakpekpe Kope 2", + "Adjoya", + "Afidegnigba", + "Afodji", + "Agbago", + "Agbedife", + "Agbenoxoe", + "Agboloukoutou", + "Agnikpe", + "Agokpo", + "Akakayi", + "Akossi Kope", + "Akpo Kope", + "Alabade 2", + "Aledzougoubi", + "Ali Kope", + "Alleluiya", + "Alou Kope 1", + "Amedeka Kope", + "Anani Kope", + "Apedome", + "Assouma Kope", + "Atchade Kope", + "Atchou Kope", + "Atikpaka Kope", + "Avakodja", + "Awayo", + "Awotere II", + "Ayedero", + "Azigo", + "Badin II", + "Banassim Kope", + "Biewo Kope", + "Bossope", + "Bro Bro", + "Dafolegname", + "Datcha", + "Degou", + "Djamde Kope", + "Djokpe", + "Dogo Kope", + "Dzidzole", + "Eko", + "Elavagnon", + "Etienne Kope", + "Fangodo", + "Filandi Kope", + "Gandokpodji", + "Gapetodzi", + "Gbaga", + "Gbedemedji", + "Gbontolo", + "Guegue", + "Igboloudja", + "Ilekohan", + "Jules Kope", + "Kagname", + "Kalanko", + "Kamaza", + "Kante Kope", + "Kayina Kope", + "Kemerida 2", + "Kogba Kope", + "Kokou Kope", + "Konadabo", + "Koubere Kope", + "Koula", + "Kpalagadja", + "Kpessi", + "Kpolikpoli", + "Lakiyekou", + "Lama Kope", + "Laoutaya Kope", + "Logba Kope", + "Macon Kope", + "Malambou", + "Mangueze", + "Mipeassem Kope", + "Modoukou Kope", + "N'konta", + "Niamtougou Kope", + "Odjo Kope", + "Odokpo Kope 3", + "Ofe Agbede", + "Ofe-Ganto", + "Ogou Allah", + "Ogou Ekpo", + "Ogou Laouda", + "Ogou-Yaka", + "Oke-Ekpa", + "Okpo-Tembe", + "Olosse-1", + "Ototoyi Kope", + "Pilibina", + "Sar-Kope", + "Sentier Kope" + ], + "Haho": [ + "Abala", + "Adaba Kope", + "Adekpe", + "Adjevihoe", + "Adzeoda Kope", + "Afram Kope", + "Agbave", + "Agbitata", + "Agnam Kope", + "Agokpa", + "Agossou Kope", + "Agoutodji", + "Akakpo Kope", + "Aketehouin", + "Akpakpakpe Centre", + "Alagbadjame", + "Alati Mondjinou Kope Alati Marche. Alat", + "Alidou Kope", + "Aloume Kope", + "Amedome Kope", + "Amouzou Kope", + "Ankou Kope", + "Asrama", + "Atali Kpota 1", + "Atchavi Kope", + "Atitsohoe", + "Attidekawoave", + "Avegodo 2", + "Ayivi Kope", + "Bagnan Kope", + "Bassinlao", + "Benoit Kope", + "Bocco Kope", + "Boutame Moba Kope", + "Dadegla Kope", + "Dansouhoe", + "Deleguehoe", + "Didome Moba Kombate Kope", + "Djakpata", + "Djatekpe", + "Djemegni", + "Dokpohoe", + "Eda Djogbessi", + "Egahoe", + "Ekpahoe", + "Essouhoe 2", + "Fadam Agassa", + "Fetsou Kope", + "Gakpatcha", + "Gbekanhoe", + "Gnanmegbe", + "Golowou", + "Guinsouhoe", + "Have", + "Hloyohoe", + "Houno Kope", + "Ibrahim Kope", + "Kadambara Kope", + "Kapando Kope", + "Kedjihoe", + "Kewoume", + "Kodjotse Kope", + "Kokou Dave 2", + "Kolani Abou", + "Kombate Kope", + "Komlan Kope", + "Kossihoe", + "Kotokou", + "Kougnowou", + "Kpali Kope 2", + "Kpedegni 2 Kofague Kope", + "Kpedjivi Kope", + "Kpele", + "Kpodji", + "Kpove Avedji", + "Lambony Kope", + "Laware", + "Logkpe", + "Louis Kope", + "Makare Afiyao Kope", + "Massadjihoe", + "Meganhoe", + "Michel Kope", + "Missaome Moba Kope 2", + "Moussahoe", + "N'tekpo Kope", + "Nossilaki Lambert Kope", + "Partawi 1", + "Podogan Kope", + "Salifou Kope", + "Sedou Kope", + "Sewahoe", + "Sodja Kope", + "Somone Kope", + "Tadonoutsou Kope", + "Taye Kpota", + "Tchao Kope", + "Tchikpe", + "Tchogoli Kope", + "Todome" + ], + "Kloto": [ + "Abaka Kope", + "Abeme Kope", + "Abouzi Kope", + "Adedze Dzigbe 3", + "Adjato Kope", + "Adzagbolou Kope", + "Afolabi Sani Kope", + "Agbe Kope", + "Agbire Kope", + "Agode", + "Agoe Kope 13", + "Agome Kpodzi", + "Agoudome 5", + "Ahossey Kope", + "Aklesso Kope", + "Akpoto Kope", + "Alaza Kossi Kope", + "Aloume Kope", + "Amedome Gonyui", + "Anago Kope", + "Antoine Kope", + "Apedome Kpatcha Kope", + "Assogbo", + "Atamaze Kope", + "Atidepe Kope", + "Atigli Agblevon Kope", + "Atta Kope", + "Aveho 4", + "Avexo 4", + "Awalegbedzi Kope", + "Ayou Komi Kope", + "Badji Kope 1", + "Bato", + "Bedou Kope", + "Bidza Kossi Kope", + "Bona Kope", + "Campement", + "Commandant Kossi Kope", + "Danyi Komi Kope", + "Derman Kope", + "Dix Kope 2", + "Dogbe Kope", + "Douane Kloto", + "Dzeve Kope", + "Ebenezer", + "Eglise Aladoura Kope", + "Enquete Kope", + "Fallah Kope", + "Foret", + "Ganadi Kope", + "Gbalave Volove", + "Germe Kope", + "Gnondomi Kope", + "Halawi Kodjo Kope", + "Hazekpessi Komi Kope", + "Honsode Kope", + "Jean Kope", + "Kadoukagna Kope", + "Kamala Kope", + "Kantchika Kope", + "Kassolas Kope", + "Katoe", + "Kelebe Kope", + "Klayi Kope", + "Koffino Kope", + "Kola Kope", + "Kossi Kpofo Kope", + "Kouma Kolie Kossi Kope", + "Kpabia Kope", + "Kpandoe Koudou Kope", + "Kpavou", + "Kpime Woume", + "Kuma Apoti", + "Lahiun Kope", + "Lifiata Kope", + "Lorah Kofitse Kope", + "Maliki Kope", + "Mariki Mayaba Kope", + "Missa Hohe1", + "N'ka Todji 1", + "Naka Kope", + "Noudzrodou Kope", + "Padaho Kope", + "Paloukouna Kope", + "Payaro Patrice Kope", + "Pidassa Kope", + "Poni Kope", + "Sala Kope", + "School Kpodji 2", + "Sidi Kope", + "Solla Kossi Kope", + "Takouvo Kope", + "Tawlessi Tchaa Kope", + "Tchasso Esso Kope", + "Tiadema Kope", + "Timati Kope", + "Togodo Sonaph", + "Tondje", + "Tsavanya", + "Tsihi Samaron Kope" + ], + "Kpele-akata": [ + "Adagali", + "Adame", + "Adekle", + "Agame", + "Ahoble", + "Akanou", + "Akpato", + "Akpokli", + "Anao", + "Anoum", + "Anoum Kona", + "Anyako Toponi", + "Avewui", + "Bete Kope", + "Dafo", + "Dali", + "Dzanife", + "Dzanife Todzi", + "Dzogbepime", + "Dzokpe", + "Gbaledze", + "Govie Apegame", + "Govie Hoeme", + "Govie Konda", + "Govie Torome", + "Kanan", + "Kayi", + "Klaxoe", + "Konda Todzi", + "Konda Torome", + "Kpato", + "Kpetatsi", + "Kponvie", + "Kponvie Todzi", + "Sonaph", + "Togodze", + "Tsavie", + "Tsoveme" + ], + "Kpélé": [ + "Ableve", + "Adeta", + "Adipom", + "Adzavedome", + "Agame Dzogbe", + "Agame Togbadza", + "Agavedzi-Hadodzi", + "Agbanon", + "Agbanon Konda", + "Agbessi Kope", + "Agbome", + "Agodeke", + "Agodome", + "Agogome", + "Agohoe", + "Agote", + "Agoutoto", + "Ahatse", + "Akadzame", + "Ali Kope", + "Alpha Kope", + "Anawo", + "Anyigbega", + "Aratoe", + "Assi Kope", + "Atime", + "Atime Dodi", + "Atrave", + "Avegame", + "Avegbadza", + "Avegbadze", + "Avovloe", + "Azio", + "Azioletsu", + "Azionou", + "Azionou 1", + "Azionou 2", + "Azionou 3", + "Bagna-Bagna", + "Beme", + "Beme 2", + "Bodze", + "Damonou", + "Detoe", + "Digui Kope", + "Djanta Kope Centre", + "Djanta Kope Ferme", + "Djave", + "Dodi 1", + "Dodi Avegame", + "Dongo Kope", + "Donomade", + "Dougba", + "Dzogbega", + "Eleme", + "Elo", + "Elo Zidjoe", + "Eve", + "Eve Torome", + "Fometou Kope", + "Foto Nord", + "Fotowui", + "Gadzale", + "Gali Kope", + "Gbaledze", + "Godzo", + "Goudeve", + "Goudeve Todome", + "Goudeve Todzi", + "Guebakui", + "Guebakui-Barrage", + "Guezere Kope", + "Haveme", + "Hlihata", + "Hlonvie", + "Hounougbe Kope", + "John Kope", + "Kalexoe", + "Kefa Kope", + "Kilim Kope", + "Koba Todzi", + "Kodzo Kope", + "Konda", + "Kougban", + "Kouwawouro", + "Kpegbadjadji", + "Kpeglo", + "Kpekpeta", + "Kpele-Ele", + "Kpeledzi", + "Layikpodzi", + "Lititoe 1", + "Lititoe 2", + "Logloe", + "Logloemegbe", + "Mama Kope", + "Mayikou", + "Medze", + "Nunya 1", + "Nunya 2" + ], + "Moyen-Mono": [ + "Adanlehoui 1", + "Adanlehoui 2", + "Adanyihohoe", + "Adihahoe", + "Adjikanme", + "Adodohoe", + "Afantchaohoe", + "Afatsaohoe", + "Afidegnigban", + "Afotrokpe I", + "Afotrokpe II", + "Agave", + "Agave Konda", + "Agave Torome", + "Agbanvikpli", + "Agbassague", + "Agblega", + "Aglamassoe", + "Agogome", + "Agossouhoe", + "Ahassome", + "Ahoantehoe", + "Ahossouhoe", + "Akeme", + "Aklotsi", + "Akpalokuenou", + "Alagbame", + "Alowanou Kope", + "Ameto", + "Amewoudjihoe", + "Andjavi", + "Aoutele", + "Atifoutou", + "Atikokpui", + "Avedjeme", + "Avedji", + "Aveho", + "Azevou", + "Azonkplehoe", + "Babakope", + "Babave", + "Bayame", + "Bessame", + "Bokohoe", + "Botokohoe", + "Bovime", + "Chiyime", + "Dagahoe", + "Daganhoe", + "Danhossouhoe", + "Davlemihoe", + "Deglehoe", + "Dekade", + "Denouhoe", + "Detokpo", + "Djidjandonou", + "Djikame", + "Djimakope", + "Dobloehoe", + "Dogbalouhoe", + "Dogblodihoe", + "Doloume", + "Douvihoe", + "Drodoume", + "Dzeibo", + "Dzeibo Totame", + "Dzobohoe", + "Dzogbefeme 1", + "Dzogbefeme 2", + "Egahoe", + "Egohoe", + "Eklohoe", + "Essehoe", + "Essouhoe", + "Fanglohoe", + "Fangnohoe", + "Fantome", + "Fiossouhoe", + "Gahonou", + "Game", + "Gangadehoe", + "Gayi Kope", + "Gbogbou", + "Gbondjinhoe", + "Glossohoe", + "Goudeve - Agote", + "Goudeve -Aveho", + "Goudohoe", + "Gozuhoe", + "Halime", + "Hetigome", + "Hodjiguehoe I", + "Hodjiguehoe II", + "Hoechilohoe", + "Hoekehoe", + "Hoenouhoe", + "Hometowouhoe", + "Hossouhoe", + "Houinssouhoe", + "Hounmenouhoe" + ], + "Ogou": [ + "Abessanhoun", + "Abogo Kope", + "Adanka Niamtougou", + "Adjambroukou", + "Adjimakpe 2", + "Adokame 1", + "Adontere Zomblewou Kope", + "Afidegnigban", + "Agbadou", + "Agbandjezo", + "Agbedrafo", + "Agogo Kope Et Kammi Kope", + "Agoudoumi3", + "Agounke Kope", + "Akakayi", + "Akodessewa 1", + "Akpaka", + "Akpo Kope", + "Albarka-Ize", + "Amedeka Kope", + "Amou Agbossou", + "Amoutchou Edoh", + "Anto Kope", + "Assosso Kope", + "Atangbasse", + "Atchakoe 2", + "Atessou", + "Atokodje", + "Ave Maria", + "Awoure Kope", + "Azovou", + "Bagaou", + "Banka Kope", + "Bavou", + "Blakpa", + "Boulali", + "Danssouhoe", + "Datcha Tchogli", + "Djidjole", + "Djonougbe", + "Egbedjrovi", + "Fafehoe", + "Fiave", + "Fodjoaye", + "Foulani Kope", + "Gbako Monou", + "Gbezounkehoe", + "Glive 2", + "Gros Kope", + "Gueme Kope", + "Hetre", + "Iko Afi", + "Iko Awokou", + "Ikoe-Ali Kope", + "Ile Agborou", + "Ile Dogo", + "Ile Issa", + "Ile N'tare", + "Ile Tengue", + "Ilekpa", + "Kable Kondji", + "Karata 2", + "Katchatina Kope", + "Katore 2 Bale Yaovi Kope", + "Kelekpe N'gberi Fala Kope", + "Kidjanta Lom-Nava", + "Klitcha Kope", + "Kodjatchan Perimetre", + "Kotadjou", + "Koudoumsa Kope", + "Koyohoe", + "Kpalakparo", + "Kpaloki-2", + "Kpeli", + "Kpogan 2", + "Lakignane Kope", + "Lassegnon Falalao", + "Lodji", + "Lom-Nava 2", + "Madjemakou", + "Maromi", + "Mawuli Kope 2", + "Moba Kope", + "N'gbafo2", + "Nyative-Edonhoe", + "Obi", + "Ogbede 1 Onouadje", + "Ohoundje", + "Okoutamati", + "Ountivou", + "Sahou", + "Sato Kope", + "Sognon Kope", + "Sossouhoe", + "Talo Gbomaho", + "Tchagala Kope", + "Tchangana Kope", + "Tchilla 1", + "Tikple Kope", + "Togblo" + ], + "Wawa": [ + "Abalo Konta", + "Aboreh", + "Adignina Kope", + "Adomi Abra", + "Afidegnigban", + "Agadza", + "Agbedohien", + "Agbo Kope", + "Ahlon", + "Akloa", + "Akongo Kope", + "Akouti Kope", + "Akpani", + "Akrokoutse", + "Alossonou Kope", + "Anonoe", + "Atikpa", + "Avegame", + "Aziezou", + "Bayome", + "Bena Ola", + "Benali", + "Bethel", + "Botokou", + "Boussoumoudie Kope", + "Chantier", + "Dafolenyame", + "Dane Kope", + "Denteh Kope", + "Djalouma", + "Djigbondi Kope", + "Doume", + "Dzodzi Godo", + "Dzogbe Cope", + "Efoukpani", + "Eketo Elavagnon", + "Elavagnon- Ikobi", + "Eledji", + "Enyilavasse", + "Etebe", + "Fanse Kope", + "Gadjitou", + "Gbadi Ediwlou", + "Gbadi Menou", + "Gbadi Onena", + "Gbedjimon Bas", + "Gbohoho", + "Gobe", + "Gonobe", + "Gosen", + "Guin Kope", + "Hossi Kope", + "Igodo", + "Ikoutsebe", + "Ikpini", + "Imougbo", + "Isobe", + "Itodou", + "Itodou Haut", + "Izobeto", + "Kakpa", + "Kamboli Kope", + "Kayibouaboua", + "Kessibo", + "Kessibo Jonhson Kope", + "Kessibo Outima", + "Kessibo Sewa", + "Keteme", + "Klabe Adape", + "Klabe Azafi", + "Klabe Olewlou", + "Klabe Owlouka", + "Komitse Kope", + "Koulassi Kope", + "Kpando", + "Kpedjinalele", + "Kpete Bibi", + "Kpete Mempeassem", + "Kpobe", + "Lom-Nava", + "Louis Kope", + "Mabeli", + "Mama Kope", + "Mangoassi", + "Menou", + "Mission Kope", + "N'tarre Kope", + "Noumatchi", + "Noviekou Kope", + "Nyawota", + "Obeka", + "Obillale Kope", + "Odokpa", + "Ofe", + "Oga Zogbegan", + "Okobi", + "Okouabia", + "Okpanahlou", + "Okplangniko", + "Okponabe" + ] + }, + "Savanes": { + "Cinkassé": [ + "Babidjoal", + "Babigou", + "Babikpergou", + "Bargounde", + "Batpotcha", + "Bingoula", + "Boade Centre", + "Bondjantchale Ou Bambongue", + "Boussanga 2", + "Datoulonte", + "Djalotougou", + "Djeguene", + "Fanworgou", + "Gabongbong", + "Gaboni", + "Ganga I", + "Ganga II", + "Garo", + "Gbatemanou", + "Gnakpoung", + "Gnoaga Centre", + "Gouloungoussi Centre", + "Horiwouri", + "Kalagou", + "Kalao I", + "Kalkantide", + "Kalmontongue", + "Kangbe", + "Kangbetchoak", + "Karbouka", + "Kodago", + "Kodinzoaga", + "Kodinzoaga II", + "Koknogo", + "Komologo", + "Konton Yobe", + "Kossou", + "Koultam'se", + "Koupelgou", + "Kpague", + "Kpalbiague I", + "Kpalbiague II", + "Kpandjoare 1", + "Kpandjoare2", + "Kpedjente", + "Kpendjieme", + "Liyietialou", + "Lonlongou", + "Mompoat Moba", + "Mompoate Mossi", + "Monba", + "Nabintuite", + "Nadjonndore", + "Nadjoudjoate", + "Nadjoundi", + "Nalongue", + "Nandjoundi Centre", + "Nanlogue 2", + "Nanlongue 1", + "Nansongue", + "Napougou", + "Nassable", + "Nassiegou", + "Natinga", + "Natone", + "Oando", + "Pilogou Centre", + "Pilougou", + "Pizosga", + "Quartier Mossi", + "Safobe 1", + "Safobe 2", + "Sam-Naba", + "Samomoni", + "Samomoni Mossi", + "Sandakoane", + "Sandogo", + "Sankargou", + "Sawardjoaga", + "Silmissi", + "Solnabe", + "Soukoudoum", + "Tabi", + "Tammondjoare Ouest", + "Tampilengo", + "Tantchingou", + "Tchimbiamggbongbong", + "Timangue", + "Timbou - Centre", + "Todjengue", + "Tonga", + "Wambagou", + "Wanre", + "Yambangbangbong", + "Yiego", + "Yiegou", + "Yikoutin", + "Yoakpate", + "Yorgou", + "Zamssi" + ], + "Kpendjal": [ + "Alhadji-Koula", + "Bagre-Kouampante", + "Bagre-Tambima", + "Bagre-Tantchal", + "Bambore", + "Bamkarmouaga", + "Bantigou", + "Barwaga", + "Betemanou", + "Blamonga", + "Bombengou", + "Bonaga", + "Bonsoale", + "Borgou Centre", + "Bouldjoare", + "Brougou", + "Dayandi-Pishou", + "Djabdjoare", + "Djagoltoli", + "Djamon-Koula", + "Djantchogou", + "Djatinga", + "Djibonli", + "Djifag-Deni", + "Djikogou", + "Djimanga", + "Djiyega", + "Djouatou", + "Donga", + "Dore", + "Douanli", + "Emondeni", + "Enamoufale", + "Ferme", + "Ferme De Brougou", + "Ferme Mossi", + "Ferme Peuhl", + "Ferme Peuhl1", + "Ferme Peuhl2", + "Ferme Peuhl3", + "Gampo", + "Gari-Koula", + "Gbantchal", + "Gnalma", + "Gnanle", + "Gnasali", + "Gniangou", + "Hameau De Teliga", + "Hameau Moba", + "Hameau Mossi", + "Iwatisonga", + "Kamou", + "Kandjieb Koula", + "Kanna-Koula", + "Kariara", + "Kokodongou", + "Komnan- Koula", + "Kondanodeni", + "Konika", + "Kontenga", + "Koudjo Koula", + "Koulyonli", + "Koumpali", + "Koundjoare", + "Kountcheli", + "Kpakounga", + "Kpakpalga", + "Kpantali", + "Kpaporga", + "Kparben", + "Kpengnagou", + "Kpenkankandi", + "Kpentindjoaga", + "Kpindjinga", + "Kpintinga", + "Lalabiga", + "Lardja Koula", + "Maldjoare", + "Mandoague", + "Mandouri", + "Mantchabiga", + "Margbongbong", + "Missili", + "Missili Peuhl", + "Mokoanne-Koula", + "Momba", + "Mondofoali", + "Monibe", + "Mossi 1", + "Mossi 2", + "Mossi Sig-Siba", + "Mossi-Dobogou", + "Moukaga", + "Nabore", + "Nadela", + "Nadjog", + "Nakpafoaga", + "Nalianou", + "Nalianoubag", + "Namoufouali" + ], + "Kpendjal-Ouest": [ + "Balbiaga", + "Balsongui", + "Bamone", + "Banangandi", + "Batbogou", + "Bendjoal", + "Biagou", + "Bolga", + "Bondjoire", + "Bongongona", + "Bonkarle", + "Bonkoungou", + "Bonloare", + "Bonsonle", + "Bontogbangbangou", + "Botolonga", + "Boulbenli", + "Boulga", + "Boumbounli", + "Boumoane", + "Dielo", + "Digban-Gbangbangou", + "Diyob", + "Djagoga", + "Djakoum Gbangbangou", + "Djambengou", + "Djamonbagou", + "Djamotiga", + "Djankougli", + "Djapiengou", + "Djapientambigou", + "Djatigou", + "Djibonli", + "Djibontoti", + "Djigniandjoga", + "Djimombendi", + "Djoamon", + "Dolongou", + "Edoga", + "Egabonga", + "Enoina", + "Ferme", + "Ferme Bomboma", + "Ferme Peuhl", + "Fobenga", + "Gabounou", + "Galbagou", + "Gale", + "Gale Centre", + "Galpoli", + "Gbaligue", + "Gbamonte", + "Gbandjale", + "Gbanga", + "Gbanlorgou", + "Gnanyome", + "Gnintwole", + "Gninyargou", + "Gniyargou", + "Kadegli", + "Kalonkalgou", + "Kankamombiagou", + "Kankampieni", + "Kankandi", + "Kantchal", + "Koidjoare", + "Kompit-Bong", + "Konangou", + "Kongbanle", + "Konkagou", + "Kouampante", + "Kourdjoaga", + "Koutidjoaga", + "Kpankangouani", + "Kpatchale", + "Kpembole", + "Kpembona", + "Kpembonle", + "Kpempiegou", + "Kpempigou", + "Kpendjaga", + "Kpendjague", + "Kpengongondi", + "Kpenkounga", + "Kpentindjoak", + "Kpentinga", + "Lamboundi", + "Lidole", + "Maldjoaga", + "Malgbongou", + "Nabangou", + "Nabatiga", + "Nadano", + "Nadjote", + "Nadjoungou", + "Nadoungou", + "Nagbati", + "Naki Centre", + "Nakpatangou", + "Nakpatchal" + ], + "Oti": [ + "Agnina", + "Akoromabra", + "Akpossou", + "Ando", + "Bandjouni", + "Barkoissi Centre", + "Bawaga", + "Biaga", + "Bieligue", + "Bigou", + "Boasse", + "Boate", + "Bolga", + "Bonzoukou", + "Boudori", + "Boula Yinli", + "Boulori", + "Dankour", + "Djabengou", + "Djagori", + "Djalondi", + "Djiboli", + "Dogbogue", + "Doman", + "Doubogue", + "Doungou", + "Fare Centre", + "Fare Haut", + "Ferme De Tiekou", + "Ferme Peulh", + "Ferme Peulh De Tchanfieri", + "Galangashie", + "Galangashie Fomboro", + "Gayerkou", + "Gbangbamona", + "Gbemba Bas", + "Gbemba Haut", + "Gnangbandi", + "Gniboutigou", + "K Adjitieri 2", + "Kadjitieri 1", + "Kampoat", + "Kankangou", + "Katial", + "Kerekete", + "Kokoumbou", + "Kong", + "Konkoagou", + "Koudjori", + "Koumbeloti", + "Koumongoukan", + "Kounkogou", + "Kouwakou 1", + "Kpagnon", + "Kpambone", + "Kpandjini", + "Kparboli", + "Kpebonga", + "Kpelo", + "Kpembonne", + "Kpensammob", + "Kpewele", + "Magna", + "Magna Momoti", + "Mamanga", + "Mantche", + "Mayefoukou", + "Miessou", + "Mobassou", + "Nabab 1", + "Nabab 2", + "Nabtongue", + "Nadoti", + "Nagbati", + "Nagbati-Sangbana", + "Nagbeni-Centre", + "Nagolkou", + "Nagouni", + "Nakolgue", + "Nakorkou", + "Nakpari Kounkoak", + "Nakpiekou", + "Nambouessou Centre", + "Namiele", + "Namiôni", + "Namoubaga 1", + "Namoubaga 2", + "Nantchare", + "Napelkangue", + "Nassiegou I", + "Nassiegou II", + "Natchakou", + "Natengolka", + "Nawakassou", + "Padori", + "Payoka", + "Payokou", + "Poloti", + "Poporokou", + "Sadori-Nagbakou" + ], + "Oti-Sud": [ + "Abieribossou", + "Akouna Assarigbe", + "Anoufoussou", + "Ayikpere", + "Bakanou", + "Bantom", + "Bassambodo", + "Batori", + "Bikonbando", + "Bogou", + "Bonani", + "Boni", + "Bor", + "Bounlandi", + "Dakorga", + "Dakpali Centre", + "Danwoga", + "Dileka", + "Djadodo", + "Djakpani", + "Djalore", + "Djandjatie", + "Djanhale", + "Dje-Bouri", + "Djinga", + "Djoni", + "Djoundjougson", + "Douare", + "Famboro", + "Ferme 1 De Namati", + "Ferme 2 De Lama", + "Ferme 2 De Satigbale", + "Ferme De Danhaka", + "Ferme De Lenido", + "Ferme Peul De Sandja Bossou", + "Ferme Peulh De Djadodo", + "Fiegou", + "Fme. De Kounkounkpe Oti 2", + "Gbatene", + "Gbenlonli", + "Gnangbandi", + "Gnoli", + "Hameau 1 De Manta", + "Hameau De Nagolgou", + "Hameau Mossi De Nayo", + "Hameau Peulh De Nambossi", + "Ibalsendja", + "Kadjitoga", + "Kamantag", + "Kanfiega", + "Kankago", + "Karmossou", + "Katchangou", + "Kignag", + "Kohanga", + "Kokou-Bossou", + "Komanou", + "Korokou", + "Kougberk", + "Kougneri", + "Koumateng", + "Koumongou", + "Kounassiegou", + "Kounkoni", + "Kounkounkpe Oti 2", + "Kountoire", + "Koussigou", + "Kpakpabou", + "Kpare", + "Kpatiboli", + "Kpen-Pouol", + "Kpentchani", + "Kpong", + "Lamba Kome", + "Legbando", + "Liboli", + "Lipoulike", + "Mabou", + "Mangbal", + "Moba", + "Mossi", + "N'foutche", + "N'kpe 1", + "N'likpa", + "Naboli", + "Naboutiebou", + "Nadaga", + "Nadjabossou", + "Nagangou", + "Nagbekti", + "Nakitoni", + "Nakpagdjoaga", + "Nakpokle", + "Nali Centre Hango", + "Nalokou", + "Namati", + "Namoni", + "Namoukou-Zongo", + "Nandiki", + "Nantiega" + ], + "Tandjoaré": [ + "Bagou Centre", + "Bakpang-Souka", + "Bamatik", + "Batia-Baok", + "Bembengou", + "Beung", + "Bintoan", + "Bogou Centre", + "Bonkoungou", + "Bore", + "Bougou", + "Boumboumigou", + "Boumboumone", + "Bounfiag", + "Brouk", + "Dagou", + "Djabargou", + "Djabiane", + "Djabire", + "Djalire", + "Djamoni", + "Djapak-Mamproussi", + "Djapal Haut", + "Djendja Tomongue", + "Dokle", + "Dote", + "Doukpergou", + "Ferme Peulh", + "Gare", + "Gbamonna", + "Gbankone", + "Gbetemong", + "Gnoate", + "Gnoumongue 1", + "Goundoga-Centre", + "Kamoa-Koinne", + "Kankangbane", + "Kerekete 1", + "Kolk I", + "Kombina-Koinne", + "Konkogou", + "Kossarik", + "Kouba", + "Koubogou", + "Koukomoni", + "Koulmatik-Myr", + "Koulougue", + "Koundoungou", + "Kounkogou", + "Kounkomoni 2", + "Kpambouloa", + "Kpanwang", + "Kpebonga", + "Kpekdoune", + "Kpelo", + "Kpembonga", + "Kpengnagou", + "Kpierik", + "Kpinkpargou", + "Kpinkparpak", + "Kumbog", + "Larbik-Baok", + "Liek", + "Loankpongou", + "Lokpano-Centre", + "Louanou", + "Malagbete", + "Mamproug", + "Mandagou", + "Matougou", + "Molbague", + "Monne", + "Moumoane-Centre", + "Myr", + "Nabame", + "Nabina", + "Nabine", + "Nada Dore", + "Nadjir", + "Nadjogou", + "Nadouanou", + "Nagniar", + "Nagouni", + "Nakpabak", + "Nakpagli Konkogou", + "Nalongue", + "Namiyala", + "Nandobik", + "Nandoga Centre", + "Nankpongorgou", + "Nannague", + "Nano-Haut", + "Nansong", + "Napote Bagou", + "Nassiagou", + "Nassiette", + "Natambok-Tougou", + "Natomone", + "Nayergou Haut", + "Nokbak" + ], + "Tône": [ + "Afounga", + "Badore I", + "Bagname IV", + "Bagname-Talbounte", + "Barkok", + "Bating", + "Bidjenga", + "Bognangou", + "Boldigbete", + "Bongougonna", + "Boul Pela", + "Boundjoare", + "Dabogou", + "Dakpante III", + "Dampiong", + "Dassoute- Centre", + "Dassoute-Kounkogue", + "Djabargou", + "Djagle Koine", + "Djammiere", + "Djeni-Koine", + "Fal- Nhague", + "Gabongbong", + "Gbal Nhague", + "Gbatanague", + "Gnabadjoani", + "Gninyetgou", + "Gnongou", + "Goussiesse", + "Ignasangue", + "Kabilagou II", + "Kambeli", + "Kankandjingue", + "Kassai-Koane", + "Koboate", + "Kongbane", + "Konkongbane", + "Korbongou-Centre", + "Koudjagou", + "Kounkoagou", + "Kounkomoni", + "Kourdjoak", + "Kpadentague", + "Kpadouaene", + "Kpatiate II", + "Kpedjini III", + "Kpenpiog", + "Labdiedo Yegname-Koine", + "Lobore", + "Louanga I", + "Mamboulik", + "Manmongou", + "Mogou-Konkoate", + "Mordjoague", + "Nabonga", + "Nadegre", + "Nagbandja", + "Nagbongou-Est", + "Nagre II", + "Nakidate II", + "Nakorgou - Tantiali", + "Naloague I", + "Nambonga-Bas", + "Namotongou", + "Nandjak", + "Nanik-Pagpo", + "Nanmongou-Yamga", + "Nassiegou", + "Natigou", + "Nioukpourma", + "Obitenlegou III", + "Opotigou II", + "Oubiagou", + "Palanbogou", + "Poamongue", + "Polatigou", + "Sabi-Zongo", + "Sanfatoute II", + "Sanfatoute-Gnybongou", + "Sankargbang", + "Sansagle", + "Sidigue I", + "Soalmongue", + "Tambisgou", + "Tangbare", + "Tanlogue", + "Tantchal", + "Tantoga I", + "Tchandana", + "Tchiet Koumbona I", + "Tidjoate II", + "Tidonte II", + "Tierou", + "Tindjote", + "Toaga-Djoale", + "Tossiegou", + "Touguibeme I", + "Toumone", + "Toutougou", + "Waldjoague" + ] + } + } +} \ No newline at end of file diff --git a/database/seeders/DemoSeeder.php b/database/seeders/DemoSeeder.php new file mode 100644 index 0000000..def0594 --- /dev/null +++ b/database/seeders/DemoSeeder.php @@ -0,0 +1,1811 @@ + 28, + 'CE2' => 30, + 'CM2' => 32, + '6ème' => 35, + '3ème' => 30, + 'Tle D' => 25, + ]; + + /** Effectif plancher pour toute classe non listée dans CLASS_SIZES. */ + private const MIN_CLASS_SIZE = 20; + + /** Programme par cycle : code matière => coefficient. */ + private const CURRICULUM = [ + 'primaire' => [ + 'FRAN' => 4, 'MATH' => 4, 'EST' => 2, 'HG' => 2, + 'ECM' => 1, 'ANG' => 2, 'EPS' => 1, 'AC' => 1, + ], + 'college' => [ + 'FRAN' => 4, 'MATH' => 4, 'ANG' => 3, 'SVT' => 2, + 'SP' => 2, 'HG' => 3, 'ECM' => 1, 'ALL' => 2, 'EPS' => 1, + ], + 'lycee' => [ + 'MATH' => 5, 'SP' => 5, 'SVT' => 5, 'FRAN' => 2, + 'ANG' => 2, 'HG' => 2, 'PHILO' => 2, 'EPS' => 1, + ], + ]; + + /** Épreuves d'une période : type de référence et coefficient. */ + private const EXAMS = [ + ['type' => 'Interrogation', 'coefficient' => 1], + ['type' => 'Devoir', 'coefficient' => 2], + ['type' => 'Composition', 'coefficient' => 3], + ]; + + private Generator $faker; + + private School $school; + + private AcademicYear $year; + + private CarbonImmutable $today; + + /** @var Collection */ + private Collection $periods; + + /** @var Collection */ + private Collection $classes; + + /** @var array */ + private array $teacherIds = []; + + /** @var array>>|null */ + private ?array $localites = null; + + /** + * Poids de recrutement hors Grand Lomé, par région. + * Le Maritime domine, puis les régions par éloignement croissant : c'est le + * sens réel des migrations internes vers la capitale. + */ + private const REGION_WEIGHTS = [ + 'Maritime' => 40, + 'Plateaux' => 25, + 'Centrale' => 15, + 'Kara' => 12, + 'Savanes' => 8, + ]; + + public function run(): void + { + if (app()->environment('production')) { + $this->command?->warn('DemoSeeder ignoré : données de démonstration interdites en production.'); + + return; + } + + $this->faker = Faker::create('fr_FR'); + $this->faker->seed(self::SEED); + mt_srand(self::SEED); + + $this->today = CarbonImmutable::now(); + + if (! $this->resolveContext()) { + return; + } + + $this->step('Barème et modèle de bulletin', fn () => $this->seedGradingConfig()); + $this->step('Enseignants', fn () => $this->seedTeachers()); + $this->step('Élèves et inscriptions', fn () => $this->seedStudentsAndEnrollments()); + $this->step('Programme et affectations', fn () => $this->seedCurriculum()); + $this->step('Évaluations', fn () => $this->seedEvaluations()); + $this->step('Notes', fn () => $this->seedMarks()); + $this->step('Moyennes par matière', fn () => $this->seedSubjectGrades()); + $this->step('Bourses', fn () => $this->seedScholarships()); + $this->step('Frais, factures et paiements', fn () => $this->seedFinance()); + $this->step('Présences', fn () => $this->seedAttendance()); + $this->step('Emploi du temps', fn () => $this->seedTimetable()); + $this->step('Calendrier scolaire', fn () => $this->seedCalendar()); + $this->step('Personnel et paie', fn () => $this->seedPayroll()); + $this->step('Dossiers courants', fn () => $this->seedCasework()); + $this->step('Bulletins', fn () => $this->seedReportCards()); + + $this->command?->newLine(); + $this->command?->info('Jeu de démonstration prêt.'); + } + + /* ------------------------------------------------------------------ */ + /* Contexte */ + /* ------------------------------------------------------------------ */ + + /** + * École, année active, périodes et classes de la démo : sans ce socle + * (posé par les seeders de référence) rien ne peut être rattaché. + */ + private function resolveContext(): bool + { + $school = School::query()->first(); + $year = AcademicYear::query()->where('active', true)->first() + ?? AcademicYear::query()->latest('start_date')->first(); + + if (! $school || ! $year) { + $this->command?->error('DemoSeeder : école ou année académique manquante. Lancez db:seed au préalable.'); + + return false; + } + + $this->school = $school; + $this->year = $year; + + $this->periods = AcademicPeriod::query() + ->where('academic_year_id', $year->id) + ->orderBy('start_date') + ->get(); + + if ($this->periods->isEmpty()) { + $this->command?->error('DemoSeeder : aucune période académique pour l\'année active.'); + + return false; + } + + // Toutes les classes actives : chacune doit être peuplée pour la démo. + $this->classes = Classroom::query() + ->with('type') + ->where('active', true) + ->get(); + + if ($this->classes->isEmpty()) { + $this->command?->error('DemoSeeder : aucune classe active (ClassroomSeeder).'); + + return false; + } + + return true; + } + + /** Cycle d'une classe, tel que référencé par self::CURRICULUM. */ + private function cycleOf(Classroom $class): string + { + $type = Str::lower($class->type?->name ?? ''); + + return match (true) { + str_contains($type, 'primaire'), str_contains($type, 'maternelle') => 'primaire', + str_contains($type, 'collège') => 'college', + default => 'lycee', + }; + } + + /** + * Effectif visé d'une classe : valeur dédiée si définie, sinon un plancher de + * 20 avec une légère variation déterministe (20 à 24) pour éviter des classes + * toutes identiques. + */ + private function targetSize(Classroom $class): int + { + return self::CLASS_SIZES[$class->code] ?? self::MIN_CLASS_SIZE + (crc32((string) $class->code) % 5); + } + + private function step(string $label, callable $work): void + { + $this->command?->line(" - {$label}..."); + $work(); + } + + /* ------------------------------------------------------------------ */ + /* Géographie togolaise */ + /* ------------------------------------------------------------------ */ + + /** + * Divisions administratives réelles du Togo, extraites de Togonou : + * 5 régions, 40 préfectures et un échantillon de localités par préfecture. + * + * @return array>> + */ + private function localites(): array + { + if ($this->localites !== null) { + return $this->localites; + } + + $path = database_path('data/togo-localites.json'); + + if (! is_file($path)) { + $this->command?->warn('togo-localites.json introuvable : les élèves seront créés sans origine géographique.'); + + return $this->localites = []; + } + + return $this->localites = json_decode(file_get_contents($path), true)['regions'] ?? []; + } + + /** + * Origine géographique d'un élève. + * + * L'établissement est à Lomé, son recrutement l'est aussi : deux élèves sur + * trois viennent du Grand Lomé (Golfe et Agoè-Nyivé), le reste du pays selon + * des poids décroissants avec l'éloignement. Un tirage uniforme sur les 40 + * préfectures donnerait une école dont un quart des élèves vient des Savanes. + * + * @return array{region: string, prefecture: string, ville: string}|null + */ + private function origin(): ?array + { + $regions = $this->localites(); + + if ($regions === []) { + return null; + } + + if ($this->faker->boolean(65) && isset($regions['Maritime'])) { + $region = 'Maritime'; + $prefecture = $this->faker->randomElement( + array_values(array_intersect(['Golfe', 'Agoè-Nyivé'], array_keys($regions['Maritime']))) + ?: array_keys($regions['Maritime']) + ); + } else { + $region = $this->weighted(array_intersect_key(self::REGION_WEIGHTS, $regions)); + $prefecture = $this->faker->randomElement(array_keys($regions[$region])); + } + + $villes = $regions[$region][$prefecture] ?? []; + + return [ + 'region' => $region, + 'prefecture' => $prefecture, + 'ville' => $villes === [] ? $prefecture : $this->faker->randomElement($villes), + ]; + } + + /** + * Tirage pondéré. + * + * @param array $weights + */ + private function weighted(array $weights): string + { + $roll = $this->faker->numberBetween(1, max(array_sum($weights), 1)); + + foreach ($weights as $key => $weight) { + $roll -= $weight; + + if ($roll <= 0) { + return $key; + } + } + + return array_key_first($weights); + } + + /** Adresse à la togolaise : quartier et repère, plutôt qu'un numéro de rue. */ + private function address(string $ville): string + { + return 'Quartier ' . $ville . ', ' . $this->faker->randomElement([ + 'face à l\'école primaire', + 'près du marché', + 'derrière la station', + 'non loin du dispensaire', + 'à côté de la pharmacie', + 'en face du terrain de football', + 'près du château d\'eau', + ]); + } + + /* ------------------------------------------------------------------ */ + /* Noms togolais */ + /* ------------------------------------------------------------------ */ + + /** + * Prénoms masculins togolais : d'abord les prénoms « jour de naissance » + * éwé/mina (majorité dans le sud et à Lomé), puis des prénoms courants, + * quelques prénoms du Nord et musulmans, et une minorité de prénoms chrétiens + * — reflet du métissage réel des registres d'état civil. + */ + private const FIRST_NAMES_MALE = [ + 'Kossi', 'Kossivi', 'Koffi', 'Kodjo', 'Kodzo', 'Komlan', 'Kokou', 'Koku', + 'Yao', 'Yaovi', 'Komi', 'Kwami', 'Mensah', 'Sena', 'Selom', 'Elom', 'Edem', + 'Etsé', 'Mawuli', 'Mawuko', 'Mawulom', 'Sitsopé', 'Delali', 'Senyo', + 'Worlanyo', 'Dodji', 'Kekeli', 'Elolo', 'Enyo', 'Sedem', 'Amevor', 'Fofo', + 'Essohanam', 'Essowè', 'Bawa', 'Kpatcha', 'Wiyao', 'Nadjombe', 'Tchalim', + 'Abalo', 'Ouro-Djeri', 'Ibrahim', 'Moussa', 'Salifou', 'Zakari', 'Aboubakar', + 'Emmanuel', 'Innocent', 'Prosper', 'Firmin', 'Célestin', 'Sylvain', + ]; + + /** Prénoms féminins togolais, même logique que la liste masculine. */ + private const FIRST_NAMES_FEMALE = [ + 'Akossiwa', 'Afiwa', 'Afiavi', 'Ablavi', 'Abla', 'Adjovi', 'Adjo', 'Adzoa', + 'Akouvi', 'Akou', 'Akoua', 'Akuvi', 'Ayaba', 'Ayawavi', 'Ama', 'Amivi', 'Ami', + 'Enyonam', 'Elolo', 'Delali', 'Senam', 'Sena', 'Akpene', 'Akpédjé', 'Sitsopé', + 'Mawunyo', 'Mawusi', 'Yayra', 'Woelali', 'Kafui', 'Essohana', 'Massan', 'Tchaa', + 'Abira', 'Fatimata', 'Aminata', 'Aïcha', 'Ramatou', 'Latifa', 'Salamatou', + 'Rafiatou', 'Zenabou', 'Delphine', 'Honorine', 'Perpétue', 'Bernadette', + 'Philomène', 'Georgette', 'Véronique', + ]; + + /** + * Noms de famille togolais : patronymes éwé/mina et guin, quelques noms du Nord + * (kabyè, tem), et des noms de familles afro-brésiliennes de Lomé (de Souza, + * d'Almeida...) bien réelles. Écrits en capitales à l'usage, comme sur les + * registres officiels. + */ + private const SURNAMES = [ + 'Adjaho', 'Agbeko', 'Agbodjan', 'Agbenou', 'Akakpo', 'Akoto', 'Amegan', + 'Amenumey', 'Ametepe', 'Amouzou', 'Anani', 'Apedo', 'Assih', 'Assignon', + 'Atsou', 'Ayité', 'Bedou', 'Dogbe', 'Dossou', 'Djondo', 'Edorh', 'Gbedemah', + 'Kougblenou', 'Kouma', 'Lawson', 'Mensah', 'Nyamadi', 'Sossou', 'Tay', + 'Tettey', 'Tossou', 'Zinsou', 'Bawara', 'Boukari', 'Djobo', 'Eklou', 'Folivi', + 'Kpatcha', 'Natchaba', 'Palanga', 'Tchangai', 'Tchani', 'Aholou', 'Attiogbé', + 'Ayassou', 'Gakpe', 'Hodabalo', 'Lokossou', 'Segbedji', 'Vigan', 'Amoussou', + 'Bassah', 'Danyo', 'Fiawoo', 'Koffigan', 'Sogadji', 'de Souza', 'd\'Almeida', + 'de Medeiros', 'Santos', 'Johnson', 'Ajavon', 'Aithnard', 'Quist', 'Vovor', + ]; + + /** Prénom togolais correspondant au sexe. */ + private function togoFirstName(string $gender): string + { + return $this->faker->randomElement( + $gender === 'female' ? self::FIRST_NAMES_FEMALE : self::FIRST_NAMES_MALE, + ); + } + + /** Nom de famille togolais (casse d'origine ; capitaliser à l'affichage si besoin). */ + private function togoSurname(): string + { + return $this->faker->randomElement(self::SURNAMES); + } + + /* ------------------------------------------------------------------ */ + /* Socle pédagogique */ + /* ------------------------------------------------------------------ */ + + /** + * Barème de notation et modèle de bulletin. + * Sans configuration active, moyennes et bulletins n'ont aucune règle de calcul. + */ + private function seedGradingConfig(): void + { + GradingConfig::firstOrCreate( + ['school_id' => $this->school->id, 'classroom_type_id' => null], + [ + 'name' => 'Barème par défaut', + 'is_active' => true, + 'passing_score' => 10, + 'default_max_score' => 20, + 'class_weight' => 1, + 'comp_weight' => 1, + 'round_precision' => 2, + 'mentions' => GradingConfig::defaultMentions(), + ], + ); + + $hasTemplate = BulletinTemplate::query() + ->where('school_id', $this->school->id) + ->whereNull('classroom_type_id') + ->exists(); + + if (! $hasTemplate) { + $this->call(BulletinTemplateSeeder::class); + } + } + + /** Corps enseignant : comptes utilisateurs + rôle, réutilisés par les affectations. */ + private function seedTeachers(): void + { + $existing = User::query()->role('enseignant')->pluck('id')->all(); + + for ($i = count($existing); $i < 14; $i++) { + // Le sexe est tiré d'abord : le prénom en découle, sinon la liste des + // enseignants affiche des « Pauline » déclarées masculines. + $gender = $this->faker->randomElement(['male', 'female']); + $firstname = $this->togoFirstName($gender); + $lastname = Str::upper($this->togoSurname()); + + $user = User::create([ + 'firstname' => $firstname, + 'lastname' => $lastname, + 'email' => 'prof' . ($i + 1) . '@dalibi.tg', + 'gender' => $gender, + 'telephone' => '+228 9' . $this->faker->numerify('# ## ## ##'), + 'address' => $this->address($this->origin()['ville'] ?? 'Lomé'), + 'password' => Hash::make('password123'), + 'is_demo' => true, + 'email_verified_at' => now(), + ]); + + $user->assignRole('enseignant'); + $existing[] = $user->id; + } + + $this->teacherIds = $existing; + } + + /* ------------------------------------------------------------------ */ + /* Élèves et inscriptions */ + /* ------------------------------------------------------------------ */ + + /** + * Complète l'effectif de chaque classe jusqu'à la cible. + * Les élèves déjà présents mais non inscrits sont récupérés avant d'en créer + * de nouveaux : une base à moitié semée ne produit pas de doublons. + */ + private function seedStudentsAndEnrollments(): void + { + $enrolledBy = User::query()->role('administrateur')->value('id') ?? User::query()->value('id'); + $sequence = Enrollment::query()->count(); + + $orphans = Student::query() + ->whereNotIn('id', Enrollment::query()->select('student_id')) + ->get() + ->all(); + + foreach ($this->classes as $class) { + $target = $this->targetSize($class); + $current = Enrollment::query() + ->where('class_id', $class->id) + ->where('academic_year_id', $this->year->id) + ->count(); + + for ($i = $current; $i < $target; $i++) { + $student = array_pop($orphans) ?? $this->createStudent($class); + + Enrollment::create([ + 'school_id' => $this->school->id, + 'student_id' => $student->id, + 'class_id' => $class->id, + 'academic_year_id' => $this->year->id, + 'enrollment_code' => 'INS-' . $this->year->year . '-' . str_pad((string) (++$sequence), 4, '0', STR_PAD_LEFT), + 'enrolled_by' => $enrolledBy, + 'enrollment_date' => $this->year->start_date, + 'status' => Enrollment::STATUS_ACTIVE, + 'academic_status' => 'en_cours', + ]); + } + } + } + + /** Élève complet : compte, dossier administratif, parents et fiche médicale. */ + private function createStudent(Classroom $class): Student + { + $gender = $this->faker->randomElement(['male', 'female']); + $firstname = $this->togoFirstName($gender); + $lastname = Str::upper($this->togoSurname()); + + // L'âge suit le niveau : un CP1 de 17 ans décrédibiliserait toute la démo. + $age = match ($this->cycleOf($class)) { + 'primaire' => $this->faker->numberBetween(6, 11), + 'college' => $this->faker->numberBetween(12, 15), + default => $this->faker->numberBetween(16, 19), + }; + $seq = Student::query()->count() + 1; + + $origin = $this->origin(); + $ville = $origin['ville'] ?? 'Lomé'; + + // Beaucoup d'élèves scolarisés à Lomé sont nés à l'intérieur du pays : + // un quart se voit attribuer une autre localité de naissance. + $naissance = $this->faker->boolean(25) ? ($this->origin()['ville'] ?? $ville) : $ville; + + $user = User::create([ + 'firstname' => $firstname, + 'lastname' => $lastname, + 'email' => 'eleve' . $seq . '@dalibi.tg', + 'gender' => $gender, + 'birth_date' => $this->today->subYears($age)->toDateString(), + 'telephone' => '+228 9' . $this->faker->numerify('# ## ## ##'), + 'address' => $this->address($ville), + 'password' => Hash::make('password123'), + 'is_demo' => true, + 'email_verified_at' => now(), + ]); + + $student = Student::create([ + 'user_id' => $user->id, + 'matricule' => 'DEM' . str_pad((string) $seq, 4, '0', STR_PAD_LEFT), + 'firstname' => $firstname, + 'lastname' => $lastname, + 'gender' => $gender, + 'birth_date' => $this->today->subYears($age)->toDateString(), + 'place_of_birth' => $naissance, + 'nationality' => 'Togolaise', + 'address' => $this->address($ville), + 'city' => $ville, + 'region' => $origin['region'] ?? null, + 'prefecture' => $origin['prefecture'] ?? null, + 'phone' => '+228 9' . $this->faker->numerify('# ## ## ##'), + 'email' => 'eleve' . $seq . '@dalibi.tg', + 'active' => true, + ]); + + StudentInformation::create([ + 'student_id' => $student->id, + 'birth_certificate_number' => 'ACT-' . $this->faker->unique()->numberBetween(100000, 999999), + 'birth_certificate_issue_date' => $this->today->subYears($age)->addMonths(2)->toDateString(), + 'birth_certificate_issue_place' => 'Lomé', + 'admission_type' => $this->faker->randomElement(['new', 'transfer', 're_admission']), + ]); + + StudentParent::create([ + 'student_id' => $student->id, + 'father_firstname' => $this->togoFirstName('male'), + 'father_lastname' => $lastname, + 'father_profession' => $this->faker->randomElement(['Enseignant', 'Commerçant', 'Agriculteur', 'Fonctionnaire', 'Chauffeur', 'Menuisier', 'Infirmier']), + 'father_phone' => '+228 9' . $this->faker->numerify('# ## ## ##'), + 'mother_firstname' => $this->togoFirstName('female'), + 'mother_lastname' => Str::upper($this->togoSurname()), + 'mother_profession' => $this->faker->randomElement(['Commerçante', 'Couturière', 'Enseignante', 'Infirmière', 'Coiffeuse', 'Secrétaire']), + 'mother_phone' => '+228 9' . $this->faker->numerify('# ## ## ##'), + 'email' => 'parent' . $seq . '@dalibi.tg', + ]); + + StudentMedicalInfo::create([ + 'student_id' => $student->id, + 'blood_group' => $this->faker->randomElement(['A+', 'A-', 'B+', 'B-', 'AB+', 'O+', 'O-']), + 'allergies' => $this->faker->optional(0.2)->randomElement(['Arachides', 'Poussière', 'Pollen', 'Fruits de mer']), + 'vaccinations' => 'DTC-Polio-Hib-HepB', + 'emergency_contact_name' => Str::upper($this->togoSurname()) . ' ' . $this->togoFirstName($this->faker->randomElement(['male', 'female'])), + 'emergency_contact_phone' => '+228 9' . $this->faker->numerify('# ## ## ##'), + ]); + + return $student; + } + + /* ------------------------------------------------------------------ */ + /* Programme */ + /* ------------------------------------------------------------------ */ + + /** Matières de chaque classe + professeur titulaire par matière. */ + private function seedCurriculum(): void + { + $subjects = Subject::query()->get()->keyBy('code'); + $slot = 0; + + foreach ($this->classes as $class) { + foreach (self::CURRICULUM[$this->cycleOf($class)] as $code => $coefficient) { + $subject = $subjects->get($code); + + if (! $subject) { + continue; + } + + ClassSubject::firstOrCreate( + [ + 'class_id' => $class->id, + 'subject_id' => $subject->id, + 'academic_year_id' => $this->year->id, + ], + ['coefficient' => $coefficient, 'group' => 'obligatoire'], + ); + + SubjectAssignment::firstOrCreate( + [ + 'class_id' => $class->id, + 'subject_id' => $subject->id, + 'academic_year_id' => $this->year->id, + ], + [ + 'teacher_id' => $this->teacherIds[$slot++ % max(count($this->teacherIds), 1)], + 'active' => true, + ], + ); + } + } + } + + /* ------------------------------------------------------------------ */ + /* Évaluations et notes */ + /* ------------------------------------------------------------------ */ + + /** + * Garantit la catégorie des types d'évaluation, dont dépend le calcul + * Classe/Compo du bulletin : la « Composition » doit être en catégorie + * `composition`, l'interrogation et le devoir en `continu`. Sans cela la + * colonne Compo du bulletin reste vide et la composition est diluée dans la + * note de classe. Défensif : les données de référence peuvent avoir dérivé. + */ + private function ensureEvaluationTypeCategories(): void + { + EvaluationType::query()->where('name', 'Composition')->update(['category' => 'composition']); + EvaluationType::query()->whereIn('name', ['Interrogation', 'Devoir'])->update(['category' => 'continu']); + } + + /** + * Une interrogation, un devoir et une composition par matière et par période. + * Les épreuves passées sont marquées « completed » ; la composition de la + * période courante reste planifiée — une démo montre aussi du travail en cours. + */ + private function seedEvaluations(): void + { + $this->ensureEvaluationTypeCategories(); + + $types = EvaluationType::query()->get()->keyBy('name'); + $classSubjects = ClassSubject::query() + ->whereIn('class_id', $this->classes->pluck('id')) + ->where('academic_year_id', $this->year->id) + ->get(); + + foreach ($this->periods as $period) { + $start = CarbonImmutable::parse($period->start_date); + $end = CarbonImmutable::parse($period->end_date); + + // Contrôle continu réparti sur la partie écoulée de la période, composition + // en fin de période : dans le trimestre en cours celle-ci reste donc à venir. + $horizon = $end->min($this->today); + $span = max((int) $start->diffInDays($horizon), 1); + + foreach (self::EXAMS as $index => $exam) { + $type = $types->get($exam['type']); + + if (! $type) { + continue; + } + + $date = $index === 2 + ? $end + : $start->addDays((int) round($span * ($index === 0 ? 0.3 : 0.7))); + + $template = EvaluationTemplate::firstOrCreate( + [ + 'academic_period_id' => $period->id, + 'evaluation_type_id' => $type->id, + 'name' => $exam['type'] . ' — ' . $period->name, + ], + [ + 'coefficient' => $exam['coefficient'], + 'max_score' => 20, + 'date' => $date->toDateString(), + ], + ); + + $isPast = $date->lessThanOrEqualTo($this->today); + + foreach ($classSubjects as $classSubject) { + Evaluation::firstOrCreate( + [ + 'evaluation_template_id' => $template->id, + 'class_subject_id' => $classSubject->id, + ], + [ + 'date' => $date->toDateString(), + 'status' => $isPast ? 'completed' : 'scheduled', + ], + ); + } + } + } + } + + /** + * Notes des épreuves déjà passées. + * + * Chaque élève reçoit un niveau propre, stable d'une matière à l'autre : sans + * cela le classement serait un tirage au sort et les bulletins n'auraient aucun + * sens. Environ 3 % d'absences pour que la colonne « absent » ne soit pas vide. + */ + private function seedMarks(): void + { + $enteredBy = User::query()->role('enseignant')->value('id') ?? User::query()->value('id'); + + foreach ($this->classes as $class) { + $studentIds = Enrollment::query() + ->where('class_id', $class->id) + ->where('academic_year_id', $this->year->id) + ->active() + ->pluck('student_id') + ->all(); + + if ($studentIds === []) { + continue; + } + + // Niveau propre à l'élève (moyenne visée), réparti autour de 11.5/20. + $levels = []; + foreach ($studentIds as $studentId) { + $levels[$studentId] = min(18.5, max(4.0, 11.5 + $this->gaussian() * 3.0)); + } + + $evaluations = Evaluation::query() + ->whereIn('class_subject_id', ClassSubject::query() + ->where('class_id', $class->id) + ->where('academic_year_id', $this->year->id) + ->select('id')) + ->where('status', 'completed') + ->pluck('id') + ->all(); + + foreach (array_chunk($evaluations, 20) as $chunk) { + $rows = []; + $known = DB::table('marks') + ->whereIn('evaluation_id', $chunk) + ->get(['evaluation_id', 'student_id']) + ->map(fn ($r) => $r->evaluation_id . '|' . $r->student_id) + ->flip(); + + foreach ($chunk as $evaluationId) { + foreach ($studentIds as $studentId) { + if ($known->has($evaluationId . '|' . $studentId)) { + continue; + } + + $absent = $this->faker->boolean(3); + $score = $absent ? null : round(min(20, max(0, $levels[$studentId] + $this->gaussian() * 2.2)), 2); + + $rows[] = [ + 'id' => (string) Str::uuid7(), + 'evaluation_id' => $evaluationId, + 'student_id' => $studentId, + 'score' => $score, + 'absent' => $absent, + 'created_by' => $enteredBy, + 'created_at' => now(), + 'updated_at' => now(), + ]; + } + } + + foreach (array_chunk($rows, 500) as $batch) { + DB::table('marks')->insert($batch); + } + } + } + } + + /** Tirage normal centré réduit (Box-Muller) : des notes qui se répartissent en cloche. */ + private function gaussian(): float + { + $u1 = max(mt_rand() / mt_getrandmax(), 1e-9); + $u2 = mt_rand() / mt_getrandmax(); + + return sqrt(-2 * log($u1)) * cos(2 * M_PI * $u2); + } + + /** + * Moyenne consolidée par matière et par période, dérivée des notes saisies. + * + * L'écran « Notes » lit la table `grades`, distincte des notes d'épreuves : + * sans cette consolidation il resterait vide alors que les bulletins, eux, + * se calculent depuis les évaluations. La moyenne est pondérée par le + * coefficient de l'épreuve, comme le fait la saisie manuelle. + */ + private function seedSubjectGrades(): void + { + $computed = DB::table('marks as m') + ->join('evaluations as e', 'e.id', '=', 'm.evaluation_id') + ->join('evaluation_templates as et', 'et.id', '=', 'e.evaluation_template_id') + ->whereNotNull('m.score') + ->where('m.absent', false) + ->groupBy('m.student_id', 'e.class_subject_id', 'et.academic_period_id') + ->selectRaw('m.student_id, e.class_subject_id, et.academic_period_id, + round(sum(m.score * et.coefficient) / nullif(sum(et.coefficient), 0), 2) as score') + ->get(); + + $known = DB::table('grades') + ->get(['student_id', 'class_subject_id', 'academic_period_id']) + ->map(fn ($g) => $g->student_id . '|' . $g->class_subject_id . '|' . $g->academic_period_id) + ->flip(); + + $rows = []; + foreach ($computed as $row) { + $key = $row->student_id . '|' . $row->class_subject_id . '|' . $row->academic_period_id; + + if ($known->has($key) || $row->score === null) { + continue; + } + + $rows[] = [ + 'id' => (string) Str::uuid7(), + 'student_id' => $row->student_id, + 'class_subject_id' => $row->class_subject_id, + 'academic_period_id' => $row->academic_period_id, + 'score' => $row->score, + 'created_at' => now(), + 'updated_at' => now(), + ]; + } + + foreach (array_chunk($rows, 500) as $batch) { + DB::table('grades')->insert($batch); + } + } + + /* ------------------------------------------------------------------ */ + /* Dossiers courants */ + /* ------------------------------------------------------------------ */ + + /** + * Les demandes et pièces qui font la vie quotidienne d'un établissement : + * justificatifs d'absence, réclamations de notes, inscriptions aux examens + * officiels, primes du personnel et documents délivrés. Chacun de ces écrans + * resterait vide sans quoi la démonstration s'arrêterait aux gros modules. + */ + private function seedCasework(): void + { + $this->seedAbsencePermissions(); + $this->seedNoteReclamations(); + $this->seedExamRegistrations(); + $this->seedAllowances(); + $this->seedDocumentIssuances(); + $this->seedFiledDocuments(); + } + + /** + * Archives et pièces du dossier élève. + * + * Ces deux modules servent de vrais fichiers en téléchargement : une ligne + * sans fichier sur le disque produirait un 404 dès le premier clic. Chaque + * entrée est donc accompagnée d'un PDF réellement écrit sur le disque + * « secure », hors du dossier public comme en exploitation. + */ + private function seedFiledDocuments(): void + { + $archivist = User::query()->role('administrateur')->value('id') ?? User::query()->value('id'); + $tags = DocumentTag::query()->get(); + + $dossiers = [ + ['category' => 'juridique', 'title' => 'Arrêté d\'ouverture de l\'établissement', 'tag' => 'Juridique'], + ['category' => 'juridique', 'title' => 'Statuts de l\'établissement', 'tag' => 'Juridique'], + ['category' => 'rh', 'title' => 'Convention collective du personnel', 'tag' => 'Ressources humaines'], + ['category' => 'rh', 'title' => 'Règlement intérieur du personnel', 'tag' => 'Contrat'], + ['category' => 'comptable', 'title' => 'Rapport financier ' . $this->year->year, 'tag' => 'Comptable'], + ['category' => 'comptable', 'title' => 'Budget prévisionnel ' . $this->year->year, 'tag' => 'Comptable'], + ['category' => 'pedagogique', 'title' => 'Programmes officiels — cycle primaire', 'tag' => 'Pédagogique'], + ['category' => 'pedagogique', 'title' => 'Projet d\'établissement', 'tag' => 'Pédagogique'], + ['category' => 'courrier', 'title' => 'Correspondance — Inspection de Lomé Golfe', 'tag' => 'Courrier'], + ['category' => 'administratif', 'title' => 'Rapport d\'inspection pédagogique', 'tag' => 'Inspection'], + ]; + + foreach ($dossiers as $index => $dossier) { + if (ArchivedDocument::query()->where('title', $dossier['title'])->exists()) { + continue; + } + + $archivedAt = $this->today->subDays($this->faker->numberBetween(10, 300)); + $path = 'archives/demo-' . Str::slug($dossier['title']) . '.pdf'; + $size = $this->writePdf($path, $dossier['title'], $this->school->name); + + $document = ArchivedDocument::create([ + 'reference' => 'ARC-' . $archivedAt->format('Y') . '-' . str_pad((string) ($index + 1), 4, '0', STR_PAD_LEFT), + 'title' => $dossier['title'], + 'description' => 'Document de référence classé aux archives de l\'établissement.', + 'category' => $dossier['category'], + 'path' => $path, + 'disk' => 'secure', + 'original_name' => Str::slug($dossier['title']) . '.pdf', + 'mime' => 'application/pdf', + 'size' => $size, + 'retention_until' => $archivedAt->addYears(10)->toDateString(), + 'archived_by' => $archivist, + 'archived_at' => $archivedAt, + ]); + + if ($tag = $tags->firstWhere('name', $dossier['tag'])) { + $document->tags()->sync([$tag->id]); + } + } + + if (StudentDocument::query()->exists()) { + return; + } + + $pieces = ['Acte de naissance', 'Certificat de scolarité antérieure', 'Photo d\'identité', 'Carnet de vaccination']; + $students = Student::query() + ->whereIn('id', Enrollment::query()->where('academic_year_id', $this->year->id)->active()->select('student_id')) + ->inRandomOrder() + ->limit(15) + ->get(); + + foreach ($students as $student) { + foreach ($this->faker->randomElements($pieces, $this->faker->numberBetween(1, 3)) as $piece) { + $path = $student->storageFolder() . '/documents/' . Str::slug($piece) . '.pdf'; + $size = $this->writePdf($path, $piece, $student->lastname . ' ' . $student->firstname); + + StudentDocument::create([ + 'student_id' => $student->id, + 'name' => $piece, + 'path' => $path, + 'original_name' => Str::slug($piece) . '.pdf', + 'mime' => 'application/pdf', + 'size' => $size, + 'uploaded_by' => $archivist, + ]); + } + } + } + + /** Écrit un PDF de démonstration sur le disque « secure » et retourne sa taille. */ + private function writePdf(string $path, string $title, string $subtitle): int + { + // Police interne (Helvetica) plutôt que DejaVu Sans : cette dernière est + // embarquée dans chaque fichier et ferait passer une page de 6 Ko à 850 Ko, + // soit ~38 Mo de pièces de démonstration à sauvegarder pour rien. + $html = '' + . '

DOCUMENT DE DÉMONSTRATION

' + . '

' . e($title) . '

' + . '

' . e($subtitle) . '

' + . '
' + . '

Contenu fictif généré par DemoSeeder. ' + . 'Ce fichier existe pour que le téléchargement fonctionne pendant la démonstration.

' + . ''; + + $content = Pdf::loadHTML($html)->output(); + Storage::disk('secure')->put($path, $content); + + return strlen($content); + } + + /** Justificatifs d'absence, répartis sur les trois états du circuit de validation. */ + private function seedAbsencePermissions(): void + { + if (AbsencePermission::query()->exists()) { + return; + } + + $reviewer = User::query()->role('directeur')->value('id') ?? User::query()->value('id'); + $parent = User::query()->role('secrétariat')->value('id') ?? $reviewer; + + // On part des élèves réellement absents : un justificatif sans absence + // correspondante serait incohérent au moindre recoupement. + $absences = DB::table('attendance_records as ar') + ->join('attendances as a', 'a.id', '=', 'ar.attendance_id') + ->whereIn('ar.status', ['absent', 'excused']) + ->inRandomOrder() + ->limit(24) + ->get(['ar.student_id', 'a.date']); + + foreach ($absences as $index => $absence) { + $start = CarbonImmutable::parse($absence->date); + $status = match ($index % 5) { + 0, 1, 2 => 'approved', + 3 => 'pending', + default => 'rejected', + }; + + $reason = $this->faker->randomElement(['medical', 'medical', 'familial', 'autre']); + + AbsencePermission::create([ + 'student_id' => $absence->student_id, + 'requested_by' => $parent, + 'reviewed_by' => $status === 'pending' ? null : $reviewer, + 'start_date' => $start->toDateString(), + 'end_date' => $start->addDays($this->faker->numberBetween(0, 3))->toDateString(), + 'reason' => $reason, + 'description' => match ($reason) { + 'medical' => 'Consultation médicale, certificat fourni.', + 'familial' => 'Événement familial hors de Lomé.', + default => 'Motif communiqué par la famille.', + }, + 'status' => $status, + 'review_comment' => $status === 'rejected' ? 'Justificatif non transmis dans les délais.' : null, + 'reviewed_at' => $status === 'pending' ? null : $start->addDays(2), + ]); + } + } + + /** Réclamations de notes : quelques contestations, dont certaines déjà arbitrées. */ + private function seedNoteReclamations(): void + { + if (NoteReclamation::query()->exists()) { + return; + } + + $reviewer = User::query()->role('directeur')->value('id') ?? User::query()->value('id'); + + $marks = DB::table('marks') + ->whereNotNull('score') + ->where('score', '<', 10) + ->inRandomOrder() + ->limit(12) + ->get(['evaluation_id', 'student_id', 'score']); + + foreach ($marks as $index => $mark) { + $status = match ($index % 4) { + 0, 1 => 'pending', + 2 => 'approved', + default => 'rejected', + }; + + $requested = min(20, (float) $mark->score + $this->faker->randomFloat(1, 1, 4)); + + NoteReclamation::create([ + 'evaluation_id' => $mark->evaluation_id, + 'student_id' => $mark->student_id, + 'requested_by' => $reviewer, + 'reason' => $this->faker->randomElement([ + 'Une question corrigée comme fausse alors que la réponse est juste.', + 'Total des points erroné sur la copie.', + 'Une page de la copie n\'a pas été corrigée.', + ]), + 'original_score' => $mark->score, + 'requested_score' => $requested, + 'status' => $status, + 'reviewed_by' => $status === 'pending' ? null : $reviewer, + 'reviewed_at' => $status === 'pending' ? null : now()->subDays($this->faker->numberBetween(1, 20)), + 'corrected_score' => $status === 'approved' ? $requested : null, + 'correction_note' => match ($status) { + 'approved' => 'Erreur de report confirmée, note rectifiée.', + 'rejected' => 'Correction vérifiée, barème correctement appliqué.', + default => null, + }, + ]); + } + } + + /** Inscriptions aux examens officiels enregistrés, pour la classe concernée. */ + private function seedExamRegistrations(): void + { + $exams = OfficialExam::query()->where('academic_year_id', $this->year->id)->get(); + + foreach ($exams as $exam) { + $students = Enrollment::query() + ->where('academic_year_id', $this->year->id) + ->when($exam->class_id, fn ($q) => $q->where('class_id', $exam->class_id)) + ->active() + ->pluck('student_id'); + + foreach ($students as $index => $studentId) { + $exists = OfficialExamRegistration::query() + ->where('official_exam_id', $exam->id) + ->where('student_id', $studentId) + ->exists(); + + if ($exists) { + continue; + } + + OfficialExamRegistration::create([ + 'official_exam_id' => $exam->id, + 'student_id' => $studentId, + 'registration_number' => Str::upper($exam->type) . '-' . $exam->year . '-' . str_pad((string) ($index + 1), 4, '0', STR_PAD_LEFT), + 'serie' => null, + 'status' => 'inscrit', + ]); + } + } + } + + /** Primes et retenues individuelles du personnel. */ + private function seedAllowances(): void + { + if (EmployeeAllowance::query()->exists()) { + return; + } + + $author = User::query()->role('administrateur')->value('id') ?? User::query()->value('id'); + $employees = EmployeeProfile::query()->inRandomOrder()->limit(8)->get(); + + $catalogue = [ + ['type' => 'earning', 'label' => 'Indemnité de logement', 'mode' => EmployeeAllowance::MODE_FIXED, 'amount' => 25000], + ['type' => 'earning', 'label' => 'Prime de responsabilité', 'mode' => EmployeeAllowance::MODE_PERCENT_BASE, 'amount' => 10], + ['type' => 'earning', 'label' => 'Heures supplémentaires', 'mode' => EmployeeAllowance::MODE_FIXED, 'amount' => 18000], + ['type' => 'deduction', 'label' => 'Avance sur salaire', 'mode' => EmployeeAllowance::MODE_FIXED, 'amount' => 30000], + ]; + + foreach ($employees as $index => $employee) { + $line = $catalogue[$index % count($catalogue)]; + + EmployeeAllowance::create([ + 'employee_profile_id' => $employee->id, + 'type' => $line['type'], + 'label' => $line['label'], + 'mode' => $line['mode'], + 'amount' => $line['amount'], + 'reason' => $line['type'] === 'deduction' ? 'Remboursement échelonné sur trois mois.' : null, + 'starts_on' => $this->today->startOfMonth()->subMonths(3)->toDateString(), + 'active' => true, + 'created_by' => $author, + ]); + } + } + + /** Documents délivrés aux familles (certificats, attestations). */ + private function seedDocumentIssuances(): void + { + if (DocumentIssuance::query()->exists()) { + return; + } + + $templates = DocumentTemplate::query()->get(); + $issuedBy = User::query()->role('secrétariat')->value('id') ?? User::query()->value('id'); + + if ($templates->isEmpty()) { + return; + } + + $students = Student::query() + ->whereIn('id', Enrollment::query()->where('academic_year_id', $this->year->id)->active()->select('student_id')) + ->inRandomOrder() + ->limit(20) + ->get(); + + foreach ($students as $index => $student) { + $template = $templates[$index % $templates->count()]; + $issuedAt = $this->today->subDays($this->faker->numberBetween(1, 120)); + + DocumentIssuance::create([ + 'template_id' => $template->id, + 'student_id' => $student->id, + 'reference_number' => 'DOC-' . $issuedAt->format('Y') . '-' . str_pad((string) ($index + 1), 4, '0', STR_PAD_LEFT), + 'issued_by' => $issuedBy, + 'payload' => [ + 'eleve' => $student->lastname . ' ' . $student->firstname, + 'matricule' => $student->matricule, + ], + 'issued_at' => $issuedAt, + ]); + } + } + + /* ------------------------------------------------------------------ */ + /* Bourses et facturation */ + /* ------------------------------------------------------------------ */ + + /** Quelques boursiers : la remise alimente la ligne DISCOUNT des factures. */ + private function seedScholarships(): void + { + $scholarships = Scholarship::query()->pluck('id')->all(); + + if ($scholarships === []) { + return; + } + + $candidates = Enrollment::query() + ->where('academic_year_id', $this->year->id) + ->active() + ->inRandomOrder() + ->limit(18) + ->pluck('student_id'); + + foreach ($candidates as $studentId) { + StudentScholarship::firstOrCreate( + [ + 'student_id' => $studentId, + 'academic_year_id' => $this->year->id, + ], + [ + 'scholarship_id' => $this->faker->randomElement($scholarships), + 'start_date' => $this->year->start_date, + 'number_of_year' => 1, + ], + ); + } + } + + /** + * Barème de frais par classe, puis une facture par inscription. + * + * Le règlement passe par InvoiceService : reçus, écritures de caisse et statut + * de facture suivent la même logique qu'une saisie réelle au guichet. La + * répartition (soldé / partiel / impayé) donne des impayés à montrer. + */ + private function seedFinance(): void + { + $this->seedFeeStructures(); + + $invoices = app(InvoiceService::class); + $cashier = User::query()->role('comptabilité')->value('id') ?? User::query()->value('id'); + + $enrollments = Enrollment::query() + ->with('student') + ->whereIn('class_id', $this->classes->pluck('id')) + ->where('academic_year_id', $this->year->id) + ->whereNotIn('id', Invoice::query()->select('enrollment_id')) + ->get(); + + foreach ($enrollments as $enrollment) { + $invoice = $invoices->createFromEnrollment($enrollment); + + if ((float) $invoice->total <= 0) { + continue; + } + + // 55 % soldé, 30 % partiel, 15 % impayé : un recouvrement crédible. + $draw = $this->faker->numberBetween(1, 100); + $share = match (true) { + $draw <= 55 => 1.0, + $draw <= 85 => $this->faker->randomFloat(2, 0.25, 0.8), + default => 0.0, + }; + + if ($share === 0.0) { + continue; + } + + $method = $this->faker->randomElement(['CASH', 'CASH', 'MOBILE_MONEY', 'MOBILE_MONEY', 'BANK_TRANSFER', 'CHEQUE']); + $paidAt = CarbonImmutable::parse($this->year->start_date) + ->addDays($this->faker->numberBetween(0, max(1, (int) CarbonImmutable::parse($this->year->start_date)->diffInDays($this->today)))); + + $invoices->recordPayment($invoice, [ + 'amount' => round((float) $invoice->total * $share, 2), + 'payment_method' => $method, + 'paid_by' => $enrollment->student?->lastname . ' ' . $enrollment->student?->firstname, + 'paid_at' => $paidAt->toDateString(), + 'created_by' => $cashier, + 'reference_number' => $method === 'CASH' ? null : Str::upper(Str::random(10)), + ]); + } + } + + /** Frais par classe : inscription + écolage, montants croissants selon le cycle. */ + private function seedFeeStructures(): void + { + $categories = FeeCategorie::query()->get()->keyBy('name'); + + foreach ($this->classes as $class) { + $ecolage = match ($this->cycleOf($class)) { + 'primaire' => 90000, + 'college' => 135000, + default => 180000, + }; + + $amounts = [ + 'Inscription' => 15000, + 'Écolage' => $ecolage, + 'Cantine' => 45000, + ]; + + foreach ($amounts as $name => $amount) { + $category = $categories->get($name); + + if (! $category) { + continue; + } + + FeeStructure::firstOrCreate( + [ + 'academic_year_id' => $this->year->id, + 'fee_category_id' => $category->id, + 'class_id' => $class->id, + ], + ['amount' => $amount], + ); + } + } + } + + /* ------------------------------------------------------------------ */ + /* Présences */ + /* ------------------------------------------------------------------ */ + + /** + * Appel du matin sur les huit dernières semaines de cours. + * Taux de présence ~92 %, avec retards et absences justifiées pour que les + * statistiques d'assiduité et le bloc « absences » du bulletin soient parlants. + */ + private function seedAttendance(): void + { + $period = $this->periods->firstWhere('is_current', true) ?? $this->periods->last(); + $author = User::query()->role('secrétariat')->value('id') ?? User::query()->value('id'); + + $start = CarbonImmutable::parse($period->start_date)->max($this->today->subWeeks(8)); + $end = $this->today->min(CarbonImmutable::parse($period->end_date)); + + foreach ($this->classes as $class) { + $studentIds = Enrollment::query() + ->where('class_id', $class->id) + ->where('academic_year_id', $this->year->id) + ->active() + ->pluck('student_id') + ->all(); + + if ($studentIds === []) { + continue; + } + + for ($day = $start; $day->lessThanOrEqualTo($end); $day = $day->addDay()) { + if ($day->isWeekend()) { + continue; + } + + $attendance = Attendance::firstOrCreate( + [ + 'class_id' => $class->id, + 'academic_period_id' => $period->id, + 'date' => $day->toDateString(), + 'session' => 'matin', + ], + ['recorded_by' => $author], + ); + + if ($attendance->records()->exists()) { + continue; + } + + $rows = []; + foreach ($studentIds as $studentId) { + $draw = $this->faker->numberBetween(1, 100); + [$status, $late] = match (true) { + $draw <= 92 => ['present', null], + $draw <= 96 => ['late', $this->faker->numberBetween(5, 40)], + $draw <= 98 => ['excused', null], + default => ['absent', null], + }; + + $rows[] = [ + 'id' => (string) Str::uuid7(), + 'attendance_id' => $attendance->id, + 'student_id' => $studentId, + 'status' => $status, + 'minutes_late' => $late, + 'created_at' => now(), + 'updated_at' => now(), + ]; + } + + DB::table('attendance_records')->insert($rows); + } + } + } + + /* ------------------------------------------------------------------ */ + /* Emploi du temps */ + /* ------------------------------------------------------------------ */ + + /** Grille hebdomadaire : les matières de la classe réparties du lundi au vendredi. */ + private function seedTimetable(): void + { + $slots = [['07:00', '09:00'], ['09:15', '11:15'], ['11:30', '12:30'], ['15:00', '17:00']]; + + foreach ($this->classes as $class) { + $assignments = SubjectAssignment::query() + ->where('class_id', $class->id) + ->where('academic_year_id', $this->year->id) + ->get(); + + if ($assignments->isEmpty()) { + continue; + } + + $cursor = 0; + foreach (range(1, 5) as $day) { + foreach ($slots as [$from, $to]) { + // Le mercredi après-midi reste libre, comme dans la plupart des établissements. + if ($day === 3 && $from === '15:00') { + continue; + } + + $assignment = $assignments[$cursor++ % $assignments->count()]; + + TimetableSlot::firstOrCreate( + [ + 'class_id' => $class->id, + 'day_of_week' => $day, + 'start_time' => $from, + ], + [ + 'school_id' => $this->school->id, + 'academic_year_id' => $this->year->id, + 'end_time' => $to, + 'subject_id' => $assignment->subject_id, + 'teacher_id' => $assignment->teacher_id, + 'room' => 'Salle ' . $class->code, + ], + ); + } + } + } + } + + /* ------------------------------------------------------------------ */ + /* Calendrier scolaire */ + /* ------------------------------------------------------------------ */ + + /** + * Calendrier de l'année : jalons pédagogiques calés sur les périodes réelles + * (compositions, conseils de classe, remise des bulletins, congés), fêtes + * légales togolaises et sessions d'examens officiels déjà enregistrées. + * + * Les dates sont dérivées des périodes plutôt qu'écrites en dur : si l'année + * de démonstration change, le calendrier suit au lieu de pointer à côté. + */ + private function seedCalendar(): void + { + $author = User::query()->role('administrateur')->value('id') ?? User::query()->value('id'); + $first = $this->periods->first(); + $last = $this->periods->last(); + + $events = [[ + 'title' => 'Rentrée scolaire ' . $this->year->year, + 'description' => 'Accueil des élèves et reprise des cours.', + 'type' => 'event', + 'start_date' => $first->start_date, + 'color' => '#2a78d6', + ], [ + 'title' => 'Réunion de rentrée des parents', + 'description' => 'Présentation de l\'équipe pédagogique et du règlement intérieur.', + 'type' => 'meeting', + 'start_date' => CarbonImmutable::parse($first->start_date)->addWeek(), + 'start_time' => '15:00', + 'end_time' => '17:00', + 'all_day' => false, + ], [ + 'title' => 'Journée portes ouvertes', + 'description' => 'Visite de l\'établissement et rencontre avec les enseignants.', + 'type' => 'event', + 'start_date' => CarbonImmutable::parse($first->start_date)->addMonths(2), + ]]; + + // Événements du mois courant : le calendrier s'ouvre sur le mois d'aujourd'hui. + // Sans eux il paraîtrait vide alors que l'année en compte partout ailleurs — et + // on ne verrait donc aucun marquage à l'ouverture. + $now = $this->today; + $events = array_merge($events, [ + [ + 'title' => 'Réunion de coordination pédagogique', + 'description' => 'Point d\'étape du trimestre avec les enseignants.', + 'type' => 'meeting', + 'start_date' => $now->subDays(2), + 'start_time' => '16:00', + 'end_time' => '18:00', + 'all_day' => false, + 'color' => '#7c3aed', + ], + [ + 'title' => 'Journée sportive inter-classes', + 'description' => 'Compétitions sportives entre les classes.', + 'type' => 'event', + 'start_date' => $now->addDays(4), + 'color' => '#2a78d6', + ], + [ + 'title' => 'Devoirs surveillés du mois', + 'description' => 'Série de devoirs surveillés dans toutes les classes.', + 'type' => 'exam', + 'start_date' => $now->addDays(8), + 'end_date' => $now->addDays(11), + 'color' => '#eb6834', + ], + [ + 'title' => 'Sortie pédagogique', + 'description' => 'Visite éducative encadrée par les enseignants.', + 'type' => 'event', + 'start_date' => $now->addDays(14), + 'color' => '#2a78d6', + ], + [ + 'title' => 'Réunion parents-professeurs', + 'description' => 'Rencontre individuelle des familles et des enseignants.', + 'type' => 'meeting', + 'start_date' => $now->addDays(18), + 'start_time' => '15:00', + 'end_time' => '18:00', + 'all_day' => false, + 'color' => '#7c3aed', + ], + ]); + + // Jalons de fin de période : composition, conseil, bulletins, congés. + foreach ($this->periods as $period) { + $end = CarbonImmutable::parse($period->end_date); + + $events[] = [ + 'title' => 'Compositions — ' . $period->name, + 'description' => 'Épreuves de synthèse de fin de période.', + 'type' => 'exam', + 'start_date' => $end->subWeek(), + 'end_date' => $end->subDays(3), + 'color' => '#eb6834', + ]; + + $events[] = [ + 'title' => 'Conseils de classe — ' . $period->name, + 'description' => 'Délibérations et appréciations par classe.', + 'type' => 'meeting', + 'start_date' => $end->addDays(3), + 'start_time' => '08:00', + 'end_time' => '13:00', + 'all_day' => false, + ]; + + $events[] = [ + 'title' => 'Remise des bulletins — ' . $period->name, + 'description' => 'Réception des parents et remise des bulletins.', + 'type' => 'meeting', + 'start_date' => $end->addDays(7), + ]; + + $events[] = [ + 'title' => 'Congés de fin de ' . Str::lower($period->name), + 'type' => 'holiday', + 'start_date' => $end->addDays(8), + 'end_date' => $end->addDays(20), + 'color' => '#1baf7a', + ]; + } + + foreach ($this->nationalHolidays($first->start_date, $last->end_date) as $date => $title) { + $events[] = [ + 'title' => $title, + 'type' => 'holiday', + 'start_date' => $date, + 'color' => '#1baf7a', + ]; + } + + // Sessions officielles déjà saisies dans le module Examens. + foreach (DB::table('official_exams')->where('academic_year_id', $this->year->id)->get() as $exam) { + $events[] = [ + 'title' => 'Examen officiel — ' . $exam->name . ' (session ' . $exam->session . ')', + 'description' => 'Centre : ' . $exam->center, + 'type' => 'exam', + 'start_date' => $exam->exam_date, + 'color' => '#eb6834', + ]; + } + + foreach ($events as $event) { + CalendarEvent::firstOrCreate( + [ + // La date fait partie de la clé : une fête légale revient d'une + // année civile à l'autre sous le même intitulé. + 'title' => $event['title'], + 'start_date' => CarbonImmutable::parse($event['start_date'])->toDateString(), + 'academic_year_id' => $this->year->id, + ], + [ + 'description' => $event['description'] ?? null, + 'type' => $event['type'], + 'end_date' => isset($event['end_date']) ? CarbonImmutable::parse($event['end_date'])->toDateString() : null, + 'all_day' => $event['all_day'] ?? true, + 'start_time' => $event['start_time'] ?? null, + 'end_time' => $event['end_time'] ?? null, + 'color' => $event['color'] ?? null, + 'created_by' => $author, + ], + ); + } + } + + /** + * Jours fériés togolais tombant dans l'intervalle donné. + * + * @return array date ISO => libellé + */ + private function nationalHolidays(string $from, string $to): array + { + $start = CarbonImmutable::parse($from); + $end = CarbonImmutable::parse($to); + + $fixed = [ + '01-01' => 'Jour de l\'An', + '01-13' => 'Fête de la Libération nationale', + '04-27' => 'Fête de l\'Indépendance', + '05-01' => 'Fête du Travail', + '06-21' => 'Journée des Martyrs', + '08-15' => 'Assomption', + '11-01' => 'Toussaint', + '12-25' => 'Noël', + ]; + + $holidays = []; + + foreach (range($start->year, $end->year) as $year) { + foreach ($fixed as $dayMonth => $title) { + $date = CarbonImmutable::parse($year . '-' . $dayMonth); + + if ($date->between($start, $end)) { + $holidays[$date->toDateString()] = $title; + } + } + } + + return $holidays; + } + + /* ------------------------------------------------------------------ */ + /* Personnel et paie */ + /* ------------------------------------------------------------------ */ + + /** + * Fiches employés pour tout le personnel, puis les cycles de paie des trois + * derniers mois : le plus ancien payé, le précédent validé, le courant en + * brouillon — les trois états du module se voient d'un coup d'œil. + */ + private function seedPayroll(): void + { + $this->seedPayrollSettings(); + + $grade = SalaryGrade::firstOrCreate( + ['name' => 'Enseignant certifié'], + ['category' => 'Enseignement', 'echelon' => '1', 'base_amount' => 145000, 'active' => true, 'sort_order' => 1], + ); + + SalaryComponent::firstOrCreate( + ['name' => 'Indemnité de transport'], + ['code' => 'TRANSP', 'type' => 'earning', 'default_amount' => 15000, 'is_default' => true, 'active' => true, 'sort_order' => 1], + ); + + SalaryComponent::firstOrCreate( + ['name' => 'Prime de rendement'], + ['code' => 'PRIME', 'type' => 'earning', 'default_amount' => 20000, 'is_default' => true, 'active' => true, 'sort_order' => 2], + ); + + // Le personnel, c'est-à-dire les comptes porteurs d'un rôle qui ne sont pas + // le compte d'accès d'un élève. + $staff = User::query() + ->whereHas('roles') + ->whereNotIn('id', Student::query()->whereNotNull('user_id')->select('user_id')) + ->get(); + + $number = EmployeeProfile::query()->count(); + + foreach ($staff as $user) { + if (EmployeeProfile::query()->where('user_id', $user->id)->exists()) { + continue; + } + + $isTeacher = $user->hasRole('enseignant'); + + EmployeeProfile::create([ + 'user_id' => $user->id, + 'employee_number' => 'EMP' . str_pad((string) (++$number), 3, '0', STR_PAD_LEFT), + 'job_title' => $isTeacher ? 'Enseignant' : ($user->getRoleNames()->first() ?? 'Personnel'), + 'department' => $isTeacher ? 'Pédagogie' : 'Administration', + 'contract_type' => $this->faker->randomElement(['CDI', 'CDI', 'CDD']), + 'hire_date' => $this->today->subYears($this->faker->numberBetween(1, 12))->toDateString(), + 'base_salary' => $isTeacher ? $this->faker->numberBetween(130, 190) * 1000 : $this->faker->numberBetween(150, 260) * 1000, + 'payment_method' => $this->faker->randomElement(['MOBILE_MONEY', 'BANK_TRANSFER']), + 'momo_number' => '+228 9' . $this->faker->numerify('# ## ## ##'), + 'cnss_number' => 'CNSS' . $this->faker->numerify('#######'), + 'status' => 'active', + 'salary_grade_id' => $isTeacher ? $grade->id : null, + ]); + } + + $payroll = app(PayrollService::class); + $cash = DB::table('cash_accounts')->where('type', 'BANK')->value('id'); + + + foreach ([3, 2, 1] as $offset) { + $month = $this->today->subMonths($offset); + + $exists = PayRun::query() + ->where('period_month', $month->month) + ->where('period_year', $month->year) + ->where('status', '!=', PayRun::CANCELLED) + ->exists(); + + if ($exists) { + continue; + } + + $run = $payroll->generate($month->month, $month->year, 'Paie ' . $month->translatedFormat('F Y')); + + if ($offset >= 2) { + $payroll->validate($run); + } + + if ($offset === 3 && $cash) { + $payroll->pay($run->fresh(), $cash); + } + } + } + + /** + * Active les retenues légales togolaises : CNSS (4 % salarié / 17,5 % employeur) + * et ITS par tranches. Sans elles, brut et net sont égaux sur tous les bulletins + * de paie — le module s'afficherait, mais ne démontrerait rien. + */ + private function seedPayrollSettings(): void + { + $settings = PayrollSetting::current(); + + if ($settings->cnss_enabled && $settings->its_enabled) { + return; + } + + $settings->update([ + 'cnss_enabled' => true, + 'cnss_employee_rate' => 4, + 'cnss_employer_rate' => 17.5, + 'its_enabled' => true, + 'its_brackets' => [ + ['up_to' => 60000, 'rate' => 0.5], + ['up_to' => 150000, 'rate' => 7], + ['up_to' => 300000, 'rate' => 15], + ['up_to' => 500000, 'rate' => 25], + ['up_to' => 800000, 'rate' => 30], + ['up_to' => null, 'rate' => 35], + ], + ]); + + SalaryComponent::firstOrCreate( + ['name' => 'Avance sur salaire'], + ['code' => 'AVANCE', 'type' => 'deduction', 'default_amount' => 0, 'is_default' => false, 'active' => true, 'sort_order' => 10], + ); + } + + /* ------------------------------------------------------------------ */ + /* Bulletins */ + /* ------------------------------------------------------------------ */ + + /** + * Bulletins figés de la première période pour toutes les classes de la démo. + * La période courante reste ouverte : on peut la générer en direct pendant + * la démonstration sans écraser un résultat déjà validé. + */ + private function seedReportCards(): void + { + $period = $this->periods->first(); + $author = User::query()->role('administrateur')->value('id') ?? User::query()->value('id'); + + if (! $period || ! $author) { + return; + } + + $builder = app(ReportCardBuilder::class); + + foreach ($this->classes as $class) { + $builder->build($class, $period, $this->year, null, false, $author); + } + } +} diff --git a/database/seeders/StudentTestSeeder.php b/database/seeders/StudentTestSeeder.php index a45513b..d9ad3ca 100644 --- a/database/seeders/StudentTestSeeder.php +++ b/database/seeders/StudentTestSeeder.php @@ -148,7 +148,7 @@ public function run(): void 'enrollment_code' => 'INS-' . $academicYear->year . '-' . str_pad((string) ($i + 1), 4, '0', STR_PAD_LEFT), 'enrolled_by' => $enrolledBy, 'enrollment_date' => now()->toDateString(), - 'status' => $faker->boolean(75) ? 'paid' : 'unpaid', + 'status' => $faker->boolean(75) ? Enrollment::STATUS_ACTIVE : Enrollment::STATUS_PENDING, ]); } } diff --git a/package.json b/package.json index 02c9c05..b93871c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://www.schemastore.org/package.json", "private": true, - "version": "1.0.1", + "version": "1.1.0", "type": "module", "scripts": { "build": "vite build",