From d759ba4dbc24bb3464c774c5ff86b367bca37cfc Mon Sep 17 00:00:00 2001 From: horacioskrp Date: Thu, 3 Sep 2026 12:25:06 +0000 Subject: [PATCH 01/11] =?UTF-8?q?feat(seeders):=20jeu=20de=20d=C3=A9monstr?= =?UTF-8?q?ation=20complet=20pour=20une=20ann=C3=A9e=20scolaire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les seeders de référence posaient le squelette (classes, matières, barèmes) mais laissaient l'application vide : 50 élèves sans inscription, aucune note, aucune facture, aucun bulletin. Impossible de montrer Dalibi sans saisir les données à la main. DemoSeeder remplit une année scolaire cohérente sur six classes (un niveau par cycle) : 183 élèves inscrits avec dossier complet, programme et affectations des enseignants, évaluations notées selon une distribution en cloche, bulletins figés du premier trimestre, facturation passée par InvoiceService (reçus et écritures de caisse compris), présences quotidiennes, emploi du temps et trois cycles de paie couvrant les états payé / validé / brouillon. Le niveau de chaque élève est tiré une fois puis réutilisé sur toutes ses matières : sans cela le classement serait un tirage au sort et les bulletins n'auraient aucun sens. Les retenues légales (CNSS, ITS) sont activées, sinon brut et net resteraient égaux sur toute la paie. Seeder idempotent et déterministe : réexécutable sans doublon, mêmes données d'une machine à l'autre. Interdit en production comme les autres seeders de démonstration. fix(seeders): StudentTestSeeder écrivait un statut d'inscription invalide Le seeder posait status = 'paid'/'unpaid', valeurs remplacées par PENDING/ACTIVE/CANCELLED lors du changement de contrainte. La migration correspondante ignore SQLite, donc les tests passaient alors que toute inscription échouait sur PostgreSQL — d'où 50 élèves pour 3 inscriptions. --- database/seeders/DemoSeeder.php | 992 +++++++++++++++++++++++++ database/seeders/StudentTestSeeder.php | 2 +- 2 files changed, 993 insertions(+), 1 deletion(-) create mode 100644 database/seeders/DemoSeeder.php diff --git a/database/seeders/DemoSeeder.php b/database/seeders/DemoSeeder.php new file mode 100644 index 0000000..2ecc2e5 --- /dev/null +++ b/database/seeders/DemoSeeder.php @@ -0,0 +1,992 @@ + 28, + 'CE2' => 30, + 'CM2' => 32, + '6ème' => 35, + '3ème' => 30, + 'Tle D' => 25, + ]; + + /** 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 = []; + + 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('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('Personnel et paie', fn () => $this->seedPayroll()); + $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; + } + + $this->classes = Classroom::query() + ->with('type') + ->whereIn('code', array_keys(self::CLASS_SIZES)) + ->get(); + + if ($this->classes->isEmpty()) { + $this->command?->error('DemoSeeder : les classes attendues sont absentes (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', + }; + } + + private function step(string $label, callable $work): void + { + $this->command?->line(" - {$label}..."); + $work(); + } + + /* ------------------------------------------------------------------ */ + /* 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++) { + $firstname = $this->faker->firstName(); + $lastname = Str::upper($this->faker->lastName()); + + $user = User::create([ + 'firstname' => $firstname, + 'lastname' => $lastname, + 'email' => 'prof' . ($i + 1) . '@dalibi.tg', + 'gender' => $this->faker->randomElement(['male', 'female']), + 'telephone' => '+228 9' . $this->faker->numerify('# ## ## ##'), + 'address' => $this->faker->streetAddress() . ', 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 = self::CLASS_SIZES[$class->code] ?? 25; + $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->faker->firstName($gender === 'male' ? 'male' : 'female'); + $lastname = Str::upper($this->faker->lastName()); + + // 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; + + $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->faker->streetAddress() . ', Lomé', + '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' => $this->faker->randomElement(['Lomé', 'Kara', 'Sokodé', 'Kpalimé', 'Atakpamé', 'Dapaong', 'Tsévié', 'Aného']), + 'nationality' => 'Togolaise', + 'address' => $this->faker->streetAddress(), + 'city' => 'Lomé', + 'region' => 'Maritime', + 'prefecture' => 'Golfe', + '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->faker->firstName('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->faker->firstName('female'), + 'mother_lastname' => Str::upper($this->faker->lastName()), + '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' => $this->faker->name(), + '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 */ + /* ------------------------------------------------------------------ */ + + /** + * 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 + { + $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); + } + + /* ------------------------------------------------------------------ */ + /* 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, + ], + ); + } + } + } + } + + /* ------------------------------------------------------------------ */ + /* 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, ]); } } From 26464bfe36da8ac456e7485f0eaa7f4aae4fe496 Mon Sep 17 00:00:00 2001 From: horacioskrp Date: Thu, 3 Sep 2026 12:47:44 +0000 Subject: [PATCH 02/11] =?UTF-8?q?feat(seeders):=20calendrier,=20moyennes?= =?UTF-8?q?=20par=20mati=C3=A8re=20et=20dossiers=20courants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le balayage des tables après le premier jet en laissait dix vides : la démonstration s'arrêtait aux gros modules et plusieurs écrans restaient muets. Sont désormais alimentés : - calendrier scolaire (22 événements), dates dérivées des périodes réelles plutôt qu'écrites en dur, plus les fêtes légales togolaises et les sessions d'examens officiels déjà enregistrées ; - moyennes par matière : l'écran Notes lit la table `grades`, distincte des notes d'épreuves, et restait vide alors que les bulletins se calculent depuis les évaluations. La moyenne y est consolidée, pondérée par le coefficient de l'épreuve comme le fait la saisie manuelle ; - justificatifs d'absence, réclamations de notes, inscriptions aux examens officiels, primes du personnel et documents délivrés ; - archives et pièces du dossier élève, avec un PDF réellement écrit sur le disque « secure » : sans fichier, le premier clic sur « télécharger » renverrait un 404. Ces PDF utilisent une police interne plutôt que DejaVu Sans, embarquée dans chaque fichier : 1,8 Ko au lieu de 850 Ko, soit 220 Ko au total contre 38 Mo à sauvegarder pour rien. fix(seeders): sexe et prénom des enseignants générés indépendamment Le sexe était tiré après le prénom, ce qui produisait des « Pauline » déclarées masculines dans la liste des enseignants. --- database/seeders/DemoSeeder.php | 556 +++++++++++++++++++++++++++++++- 1 file changed, 554 insertions(+), 2 deletions(-) diff --git a/database/seeders/DemoSeeder.php b/database/seeders/DemoSeeder.php index 2ecc2e5..3a9c964 100644 --- a/database/seeders/DemoSeeder.php +++ b/database/seeders/DemoSeeder.php @@ -2,12 +2,19 @@ namespace Database\Seeders; +use App\Models\AbsencePermission; use App\Models\AcademicPeriod; use App\Models\AcademicYear; +use App\Models\ArchivedDocument; use App\Models\Attendance; use App\Models\BulletinTemplate; +use App\Models\CalendarEvent; use App\Models\Classroom; use App\Models\ClassSubject; +use App\Models\DocumentIssuance; +use App\Models\DocumentTag; +use App\Models\DocumentTemplate; +use App\Models\EmployeeAllowance; use App\Models\EmployeeProfile; use App\Models\Enrollment; use App\Models\Evaluation; @@ -17,6 +24,9 @@ use App\Models\FeeStructure; use App\Models\GradingConfig; use App\Models\Invoice; +use App\Models\NoteReclamation; +use App\Models\OfficialExam; +use App\Models\OfficialExamRegistration; use App\Models\PayRun; use App\Models\PayrollSetting; use App\Models\SalaryComponent; @@ -24,6 +34,7 @@ use App\Models\Scholarship; use App\Models\School; use App\Models\Student; +use App\Models\StudentDocument; use App\Models\StudentInformation; use App\Models\StudentMedicalInfo; use App\Models\StudentParent; @@ -35,6 +46,7 @@ use App\Services\InvoiceService; use App\Services\PayrollService; use App\Services\ReportCardBuilder; +use Barryvdh\DomPDF\Facade\Pdf; use Carbon\CarbonImmutable; use Faker\Factory as Faker; use Faker\Generator; @@ -42,6 +54,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; /** @@ -136,11 +149,14 @@ public function run(): void $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(); @@ -253,14 +269,17 @@ private function seedTeachers(): void $existing = User::query()->role('enseignant')->pluck('id')->all(); for ($i = count($existing); $i < 14; $i++) { - $firstname = $this->faker->firstName(); + // 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->faker->firstName($gender); $lastname = Str::upper($this->faker->lastName()); $user = User::create([ 'firstname' => $firstname, 'lastname' => $lastname, 'email' => 'prof' . ($i + 1) . '@dalibi.tg', - 'gender' => $this->faker->randomElement(['male', 'female']), + 'gender' => $gender, 'telephone' => '+228 9' . $this->faker->numerify('# ## ## ##'), 'address' => $this->faker->streetAddress() . ', Lomé', 'password' => Hash::make('password123'), @@ -592,6 +611,382 @@ private function gaussian(): float 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 */ /* ------------------------------------------------------------------ */ @@ -842,6 +1237,163 @@ private function seedTimetable(): void } } + /* ------------------------------------------------------------------ */ + /* 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), + ]]; + + // 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 */ /* ------------------------------------------------------------------ */ From 84352c5fcb884df7266494c509c4cfb7b66a7948 Mon Sep 17 00:00:00 2001 From: horacioskrp Date: Thu, 3 Sep 2026 14:04:33 +0000 Subject: [PATCH 03/11] =?UTF-8?q?feat(seeders):=20villes=20r=C3=A9elles=20?= =?UTF-8?q?du=20Togo=20pour=20les=20=C3=A9l=C3=A8ves=20de=20d=C3=A9mo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les élèves étaient tous rattachés à Lomé/Maritime/Golfe avec des adresses générées par Faker (« 27 rue Roche »), sans rapport avec le Togo. La carte géographique des statistiques n'affichait qu'un point et la couverture plafonnait à 72,7 %. Ajoute un extrait des divisions administratives réelles (database/data/ togo-localites.json), tiré de la base Togonou : 5 régions, 40 préfectures et un échantillon de localités par préfecture (3 827 au total). L'extraction répare les doubles encodages UTF-8/CP850 présents dans la source, retire les précisions cadastrales entre parenthèses et écarte les 47 entrées irrécupérables. Le recrutement suit la réalité d'un établissement de Lomé : deux élèves sur trois viennent du Grand Lomé (Golfe, Agoè-Nyivé), le reste des autres régions avec un poids décroissant selon l'éloignement — un tirage uniforme aurait donné un quart d'élèves venus des Savanes. Adresses à la togolaise (quartier et repère plutôt qu'un numéro de rue). Couverture géographique portée à 100 %, 20 préfectures représentées. --- database/data/togo-localites.json | 3924 +++++++++++++++++++++++++++++ database/seeders/DemoSeeder.php | 136 +- 2 files changed, 4053 insertions(+), 7 deletions(-) create mode 100644 database/data/togo-localites.json diff --git a/database/data/togo-localites.json b/database/data/togo-localites.json new file mode 100644 index 0000000..77cca9b --- /dev/null +++ b/database/data/togo-localites.json @@ -0,0 +1,3924 @@ +{ + "source": "Togonou — divisions administratives officielles du Togo", + "hierarchie": "region > 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 index 3a9c964..b284636 100644 --- a/database/seeders/DemoSeeder.php +++ b/database/seeders/DemoSeeder.php @@ -125,6 +125,22 @@ class DemoSeeder extends Seeder /** @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')) { @@ -229,6 +245,105 @@ private function step(string $label, callable $work): void $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', + ]); + } + /* ------------------------------------------------------------------ */ /* Socle pédagogique */ /* ------------------------------------------------------------------ */ @@ -281,7 +396,7 @@ private function seedTeachers(): void 'email' => 'prof' . ($i + 1) . '@dalibi.tg', 'gender' => $gender, 'telephone' => '+228 9' . $this->faker->numerify('# ## ## ##'), - 'address' => $this->faker->streetAddress() . ', Lomé', + 'address' => $this->address($this->origin()['ville'] ?? 'Lomé'), 'password' => Hash::make('password123'), 'is_demo' => true, 'email_verified_at' => now(), @@ -353,6 +468,13 @@ private function createStudent(Classroom $class): Student }; $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, @@ -360,7 +482,7 @@ private function createStudent(Classroom $class): Student 'gender' => $gender, 'birth_date' => $this->today->subYears($age)->toDateString(), 'telephone' => '+228 9' . $this->faker->numerify('# ## ## ##'), - 'address' => $this->faker->streetAddress() . ', Lomé', + 'address' => $this->address($ville), 'password' => Hash::make('password123'), 'is_demo' => true, 'email_verified_at' => now(), @@ -373,12 +495,12 @@ private function createStudent(Classroom $class): Student 'lastname' => $lastname, 'gender' => $gender, 'birth_date' => $this->today->subYears($age)->toDateString(), - 'place_of_birth' => $this->faker->randomElement(['Lomé', 'Kara', 'Sokodé', 'Kpalimé', 'Atakpamé', 'Dapaong', 'Tsévié', 'Aného']), + 'place_of_birth' => $naissance, 'nationality' => 'Togolaise', - 'address' => $this->faker->streetAddress(), - 'city' => 'Lomé', - 'region' => 'Maritime', - 'prefecture' => 'Golfe', + '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, From 96c0f100c7ba7fcc15b17dfdddef098f8d3b7de3 Mon Sep 17 00:00:00 2001 From: horacioskrp Date: Thu, 3 Sep 2026 14:13:37 +0000 Subject: [PATCH 04/11] =?UTF-8?q?feat(seeders):=20noms=20et=20pr=C3=A9noms?= =?UTF-8?q?=20togolais=20pour=20les=20donn=C3=A9es=20de=20d=C3=A9mo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les noms venaient de Faker fr_FR (Besson, Levy, Pelletier), sans rapport avec le Togo. Ajoute trois listes de noms togolais authentiques — prénoms masculins et féminins (prénoms « jour de naissance » éwé/mina majoritaires, prénoms courants, quelques prénoms du Nord, musulmans et chrétiens) et patronymes (noms éwé/mina/guin, noms du Nord, familles afro-brésiliennes de Lomé) — puis tire tous les noms (élèves, enseignants, parents, contacts d'urgence) dans ces listes selon le sexe. Les prénoms restent cohérents avec le sexe enregistré, et le nom de famille du père suit celui de l'élève (cohérence familiale), la mère gardant son nom de jeune fille. --- database/seeders/DemoSeeder.php | 79 +++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/database/seeders/DemoSeeder.php b/database/seeders/DemoSeeder.php index b284636..b0939cd 100644 --- a/database/seeders/DemoSeeder.php +++ b/database/seeders/DemoSeeder.php @@ -344,6 +344,69 @@ private function address(string $ville): string ]); } + /* ------------------------------------------------------------------ */ + /* 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 */ /* ------------------------------------------------------------------ */ @@ -387,8 +450,8 @@ private function seedTeachers(): void // 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->faker->firstName($gender); - $lastname = Str::upper($this->faker->lastName()); + $firstname = $this->togoFirstName($gender); + $lastname = Str::upper($this->togoSurname()); $user = User::create([ 'firstname' => $firstname, @@ -457,8 +520,8 @@ private function seedStudentsAndEnrollments(): void private function createStudent(Classroom $class): Student { $gender = $this->faker->randomElement(['male', 'female']); - $firstname = $this->faker->firstName($gender === 'male' ? 'male' : 'female'); - $lastname = Str::upper($this->faker->lastName()); + $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)) { @@ -516,12 +579,12 @@ private function createStudent(Classroom $class): Student StudentParent::create([ 'student_id' => $student->id, - 'father_firstname' => $this->faker->firstName('male'), + '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->faker->firstName('female'), - 'mother_lastname' => Str::upper($this->faker->lastName()), + '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', @@ -532,7 +595,7 @@ private function createStudent(Classroom $class): Student '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' => $this->faker->name(), + 'emergency_contact_name' => Str::upper($this->togoSurname()) . ' ' . $this->togoFirstName($this->faker->randomElement(['male', 'female'])), 'emergency_contact_phone' => '+228 9' . $this->faker->numerify('# ## ## ##'), ]); From a983411a5661a49e629fe851ed893b7d7c5e34a9 Mon Sep 17 00:00:00 2001 From: horacioskrp Date: Thu, 3 Sep 2026 14:21:53 +0000 Subject: [PATCH 05/11] =?UTF-8?q?fix(bulletin):=20logo=20de=20l'en-t=C3=AA?= =?UTF-8?q?te=20trop=20grand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'en-tête ministériel du bulletin est rendu par DocumentRenderer, mais le CSS du bulletin n'embarquait aucun style `.mh-*` : le logo, sans plafond de taille, s'affichait à sa dimension naturelle (450×450 px) et écrasait tout l'en-tête. Inclut `DocumentRenderer::headerCss()` dans le CSS du bulletin — méthode déjà prévue pour les documents qui ne reprennent pas tout le baseCss. Le logo est désormais plafonné à 68 px et l'en-tête à trois colonnes (ministère, logo, république) reprend sa mise en forme (filets, tailles de police). --- app/Services/BulletinRenderer.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Services/BulletinRenderer.php b/app/Services/BulletinRenderer.php index 2946100..8b0e9b5 100644 --- a/app/Services/BulletinRenderer.php +++ b/app/Services/BulletinRenderer.php @@ -340,7 +340,10 @@ private function ordinal(int $rank): string private function css(): string { - return <<documents->headerCss() . << Date: Thu, 3 Sep 2026 14:35:11 +0000 Subject: [PATCH 06/11] =?UTF-8?q?fix(seeders):=20garantir=20la=20cat=C3=A9?= =?UTF-8?q?gorie=20des=20types=20d'=C3=A9valuation=20(colonne=20Compo)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le calcul Classe/Compo du bulletin distingue les évaluations par la catégorie de leur type : « continu » (note de classe) contre « composition » (examen). Dans la base de dev, le type « Composition » avait dérivé en catégorie « continu » — résultat : la colonne Compo du bulletin restait vide, la composition était diluée dans la note de classe, et la moyenne contournait la pondération Classe/Compo du barème (compo null → repli sur classe seul). Le seeder de référence (ExamenTypeSeeder) est correct ; c'est la donnée qui avait divergé. DemoSeeder rétablit désormais les catégories avant de créer les évaluations (ensureEvaluationTypeCategories), pour que la démo reste cohérente quelle que soit la dérive des données de référence. --- database/seeders/DemoSeeder.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/database/seeders/DemoSeeder.php b/database/seeders/DemoSeeder.php index b0939cd..322ab73 100644 --- a/database/seeders/DemoSeeder.php +++ b/database/seeders/DemoSeeder.php @@ -648,6 +648,19 @@ private function seedCurriculum(): void /* É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 @@ -655,6 +668,8 @@ private function seedCurriculum(): void */ private function seedEvaluations(): void { + $this->ensureEvaluationTypeCategories(); + $types = EvaluationType::query()->get()->keyBy('name'); $classSubjects = ClassSubject::query() ->whereIn('class_id', $this->classes->pluck('id')) From b311385bfe590656f31d68f580b413005dcccff1 Mon Sep 17 00:00:00 2001 From: horacioskrp Date: Thu, 3 Sep 2026 15:10:52 +0000 Subject: [PATCH 07/11] =?UTF-8?q?feat(bulletin):=20renommer=20la=20colonne?= =?UTF-8?q?=20=C2=AB=20Classe=20=C2=BB=20en=20=C2=AB=20Devoir=20=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le libellé par défaut de la colonne de contrôle continu passe de « Classe » à « Devoir », convention plus parlante sur les bulletins. Seul l'affichage change : la clé et la source restent « classe », donc la colonne agrège toujours le contrôle continu (interro + devoir) et la moyenne continue de combiner ce total avec la composition. Les modèles existants portant encore le libellé par défaut sont réalignés ; un libellé déjà personnalisé par une école n'est pas écrasé. --- app/Models/BulletinTemplate.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'], From acf6c8f5fbd8181d985a6bab083613e146a1d8c2 Mon Sep 17 00:00:00 2001 From: horacioskrp Date: Thu, 3 Sep 2026 15:32:23 +0000 Subject: [PATCH 08/11] =?UTF-8?q?feat(seeders):=20peupler=20toutes=20les?= =?UTF-8?q?=20classes=20actives=20(au=20moins=2020=20=C3=A9l=C3=A8ves)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le jeu de démo ne remplissait que six classes ; les quinze autres restaient vides. DemoSeeder couvre désormais toutes les classes actives, avec un effectif plancher de 20 élèves (variation déterministe 20–24 pour les classes sans effectif dédié) et les effectifs spécifiques conservés pour la variété. Toutes les étapes en aval itèrent déjà sur l'ensemble des classes : programme, affectations, évaluations, notes, moyennes, factures, présences, emploi du temps et bulletins suivent automatiquement. Résultat : 21 classes peuplées, 507 élèves, 507 bulletins, contre 183 auparavant. Seeder toujours idempotent (complément jusqu'à l'effectif cible, sans doublon). --- database/seeders/DemoSeeder.php | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/database/seeders/DemoSeeder.php b/database/seeders/DemoSeeder.php index 322ab73..25481f3 100644 --- a/database/seeders/DemoSeeder.php +++ b/database/seeders/DemoSeeder.php @@ -75,7 +75,11 @@ class DemoSeeder extends Seeder /** Graine fixe : une démo reproductible d'une machine à l'autre. */ private const SEED = 20260903; - /** Classes retenues et effectif visé (un niveau par cycle). */ + /** + * Effectifs spécifiques pour quelques classes (variété du jeu de démo). + * Toutes les autres classes actives reçoivent l'effectif par défaut de + * {@see self::targetSize()} — au moins 20 élèves. + */ private const CLASS_SIZES = [ 'CP1' => 28, 'CE2' => 30, @@ -85,6 +89,9 @@ class DemoSeeder extends Seeder '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' => [ @@ -213,13 +220,14 @@ private function resolveContext(): bool return false; } + // Toutes les classes actives : chacune doit être peuplée pour la démo. $this->classes = Classroom::query() ->with('type') - ->whereIn('code', array_keys(self::CLASS_SIZES)) + ->where('active', true) ->get(); if ($this->classes->isEmpty()) { - $this->command?->error('DemoSeeder : les classes attendues sont absentes (ClassroomSeeder).'); + $this->command?->error('DemoSeeder : aucune classe active (ClassroomSeeder).'); return false; } @@ -239,6 +247,16 @@ private function cycleOf(Classroom $class): string }; } + /** + * 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}..."); @@ -492,7 +510,7 @@ private function seedStudentsAndEnrollments(): void ->all(); foreach ($this->classes as $class) { - $target = self::CLASS_SIZES[$class->code] ?? 25; + $target = $this->targetSize($class); $current = Enrollment::query() ->where('class_id', $class->id) ->where('academic_year_id', $this->year->id) From 1b915c7c9992d85e1c364e14464ebe9c8a5d4b4a Mon Sep 17 00:00:00 2001 From: horacioskrp Date: Thu, 3 Sep 2026 15:45:43 +0000 Subject: [PATCH 09/11] =?UTF-8?q?fix(bulletin):=20colonne=20Compo=20toujou?= =?UTF-8?q?rs=20vide=20au=20rendu=20(d=C3=A9calage=20de=20cl=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La colonne « note » de source « composition » était résolue par `$line['composition']`, mais le snapshot du bulletin stocke cette valeur sous la clé « compo » (comme le renvoie subjectClasseCompo). Résultat : la colonne Compo restait vide au rendu, y compris quand la note existait — indépendamment de la période et du barème. Le rendu fait désormais correspondre la source « composition » à la clé « compo » du snapshot. Correctif purement d'affichage : aucun bulletin à régénérer, la donnée était déjà présente dans le snapshot. --- app/Services/BulletinRenderer.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Services/BulletinRenderer.php b/app/Services/BulletinRenderer.php index 8b0e9b5..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 From 7e104fef86d46181850e1f53005e46f6c2f4e46e Mon Sep 17 00:00:00 2001 From: horacioskrp Date: Thu, 3 Sep 2026 15:57:49 +0000 Subject: [PATCH 10/11] =?UTF-8?q?fix(seeders):=20=C3=A9v=C3=A9nements=20du?= =?UTF-8?q?=20calendrier=20dans=20le=20mois=20courant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le calendrier s'ouvre sur le mois d'aujourd'hui, or le jeu de démo groupait ses événements en début et fin de période — laissant le mois courant vide. À l'ouverture, la démo paraissait donc « sans marquage » alors que l'année en comptait partout ailleurs, et que le rendu colore bien par type. Ajoute cinq événements ancrés sur la date du jour (réunion de coordination, journée sportive, semaine de devoirs surveillés, sortie pédagogique, réunion parents-professeurs), de types et couleurs variés, pour que le calendrier s'affiche peuplé et coloré dès l'ouverture. --- database/seeders/DemoSeeder.php | 49 +++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/database/seeders/DemoSeeder.php b/database/seeders/DemoSeeder.php index 25481f3..def0594 100644 --- a/database/seeders/DemoSeeder.php +++ b/database/seeders/DemoSeeder.php @@ -1494,6 +1494,55 @@ private function seedCalendar(): void '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); From 090ed8cc8d258cc2837279bec17174bfc54ade4a Mon Sep 17 00:00:00 2001 From: horacioskrp Date: Thu, 3 Sep 2026 16:02:28 +0000 Subject: [PATCH 11/11] chore(release): v1.1.0 --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) 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/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",