===== STRUCTURE ===== . ├── ActionRequest.php ├── Appointment.php ├── Budget.php ├── City.php ├── Client.php ├── Cycle.php ├── DelegueSalary.php ├── Deplacement.php ├── Expense.php ├── Listing.php ├── Location.php ├── Notification.php ├── Objective.php ├── output.txt ├── Permission.php ├── Product.php ├── SalarySetting.php ├── User.php ├── VisitInProgress.php ├── Visit.php ├── VisitProduct.php ├── VisitType.php ├── WeeklyProgram.php └── WorkSession.php 1 directory, 24 files ===== CONTENU DES FICHIERS ===== ----- ./Objective.php ----- belongsTo(Cycle::class); } public function user() { return $this->belongsTo(User::class); } } ----- ./Permission.php ----- 'date', 'end_date' => 'date', 'half_day' => 'boolean', ]; protected $appends = [ 'date_debut_formatted', 'date_fin_formatted', 'isFullDay', 'motif', ]; /* ✅ Dates formatées */ public function getDateDebutFormattedAttribute() { return $this->start_date?->format('d/m/Y'); } public function getDateFinFormattedAttribute() { return $this->end_date?->format('d/m/Y'); } /* ✅ Journée entière / demi */ public function getIsFullDayAttribute() { return !$this->half_day; } /* ✅ Motif lisible */ public function getMotifAttribute() { return match ($this->reason) { 'trajet' => 'Trajet', 'reunion' => 'Réunion', 'congres' => 'Congrès', 'formation' => 'Formation', 'maladie' => 'Congé maladie', 'affaire_personnel' => 'Affaire personnelle', default => ucfirst($this->reason), }; } public function user() { return $this->belongsTo(User::class); } } ----- ./Notification.php ----- 'array', 'is_read' => 'boolean', ]; } ----- ./VisitInProgress.php ----- 'datetime', 'end_time' => 'datetime', ]; public function user() { return $this->belongsTo(User::class); } public function listing() { return $this->belongsTo(Listing::class); } } ----- ./Visit.php ----- 'boolean', 'marche_direct' => 'boolean', 'objection' => 'boolean', 'important' => 'boolean', 'admin_pending' => 'boolean', 'has_ticket' => 'boolean', 'is_far' => 'boolean', 'elapsed_time' => 'integer', 'distance_meters' => 'float', 'poslat' => 'float', 'poslong' => 'float', 'proposed_meds' => 'array', 'requested_meds' => 'array', 'results' => 'array', 'date_visit' => 'datetime:d/m/Y H:i', ]; // Relations public function user() { return $this->belongsTo(User::class); } public function listing() { return $this->belongsTo(Listing::class); } public function visitType() { return $this->belongsTo(VisitType::class); } public function cycle() { return $this->belongsTo(Cycle::class); } public function user_accompagnant() { return $this->belongsTo(User::class, 'user_accompagnant_id'); } } ----- ./Appointment.php ----- belongsTo(User::class); } public function listing() { return $this->belongsTo(Listing::class); } } ----- ./Product.php ----- hasMany(VisitProduct::class); } public function actionRequests() { return $this->hasMany(ActionRequest::class); } } ----- ./Deplacement.php ----- 'datetime', 'date_depart' => 'datetime:d/m/Y H:i', ]; public function user() { return $this->belongsTo(User::class); } } ----- ./output.txt ----- ----- ./Cycle.php ----- 'date', 'end_date' => 'date', ]; // 🧩 Relation : un cycle contient plusieurs visites public function visits() { return $this->hasMany(Visit::class); } /** * Lorsqu’un cycle est créé ou activé, les autres deviennent inactifs. */ protected static function boot() { parent::boot(); static::saving(function (Cycle $cycle) { // Si ce cycle est défini comme "actif" if ($cycle->status === 'actif') { // On désactive tous les autres cycles static::where('id', '!=', $cycle->id) ->where('status', 'actif') ->update(['status' => 'inactif']); } }); } } ----- ./Client.php ----- 'array', 'tags' => 'array', 'preferences' => 'array', 'latitude' => 'decimal:8', 'longitude' => 'decimal:8', 'credit_limit' => 'decimal:2', 'discount_rate' => 'decimal:2', 'monthly_potential' => 'integer', 'payment_terms' => 'integer', 'is_active' => 'boolean', 'is_key_account' => 'boolean', 'do_not_visit' => 'boolean', 'is_prospect' => 'boolean', 'birthday' => 'date', 'last_visit_date' => 'date', 'next_visit_date' => 'date', 'opening_time' => 'datetime:H:i', 'closing_time' => 'datetime:H:i', 'break_start_time' => 'datetime:H:i', 'break_end_time' => 'datetime:H:i', ]; /** * Boot method */ protected static function boot() { parent::boot(); static::creating(function ($client) { if (empty($client->code)) { $client->code = $client->generateUniqueCode(); } }); static::updating(function ($client) { // Mettre à jour les coordonnées si l'adresse change if ($client->isDirty(['address', 'city_id'])) { // Ici vous pouvez appeler un service de géocodage } }); } /** * Génération du code unique selon le type */ protected function generateUniqueCode(): string { $prefix = match($this->client_type) { 'doctor' => 'DOC', 'pharmacy' => 'PHA', 'clinic' => 'CLI', 'hospital' => 'HOS', 'wholesaler' => 'WHO', default => 'CLT', }; do { $number = str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT); $code = $prefix . $number; } while (self::where('code', $code)->exists()); return $code; } /** * Relations */ // Ville public function city() { return $this->belongsTo(City::class); } // Créateur public function creator() { return $this->belongsTo(User::class, 'created_by'); } // Visiteur assigné public function assignedVisitor() { return $this->belongsTo(User::class, 'assigned_to'); } // Client parent (pour groupes) public function parentClient() { return $this->belongsTo(self::class, 'parent_client_id'); } // Clients enfants (filiales) public function childClients() { return $this->hasMany(self::class, 'parent_client_id'); } // Visites public function visits() { return $this->hasMany(Visit::class); } // Commandes public function orders() { return $this->hasMany(Order::class); } // Contacts additionnels public function contacts() { return $this->hasMany(ClientContact::class); } /** * Scopes */ // Clients actifs public function scopeActive($query) { return $query->where('is_active', true) ->where('do_not_visit', false); } // Par type public function scopeOfType($query, $type) { return $query->where('client_type', $type); } // Médecins public function scopeDoctors($query) { return $query->where('client_type', 'doctor'); } // Pharmacies public function scopePharmacies($query) { return $query->where('client_type', 'pharmacy'); } // Par catégorie public function scopeCategory($query, $category) { return $query->where('category', $category); } // Comptes clés public function scopeKeyAccounts($query) { return $query->where('is_key_account', true); } // Prospects public function scopeProspects($query) { return $query->where('is_prospect', true); } // Clients d'un visiteur public function scopeAssignedTo($query, $visitorId) { return $query->where('assigned_to', $visitorId); } // Recherche public function scopeSearch($query, $search) { return $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('code', 'like', "%{$search}%") ->orWhere('phone', 'like', "%{$search}%") ->orWhere('mobile', 'like', "%{$search}%") ->orWhere('email', 'like', "%{$search}%"); }); } /** * Accesseurs */ // Type formaté public function getTypeFormattedAttribute() { return match($this->client_type) { 'doctor' => 'Médecin', 'pharmacy' => 'Pharmacie', 'clinic' => 'Clinique', 'hospital' => 'Hôpital', 'wholesaler' => 'Grossiste', default => 'Autre', }; } // Nom d'affichage public function getDisplayNameAttribute() { if ($this->client_type === 'doctor' && $this->speciality) { return "Dr. {$this->name} ({$this->speciality})"; } return $this->trade_name ?? $this->name; } // Adresse complète public function getFullAddressAttribute() { $parts = array_filter([ $this->address, $this->address_2, $this->neighborhood, $this->city?->name ]); return implode(', ', $parts); } // Coordonnées GPS public function getCoordinatesAttribute() { if ($this->latitude && $this->longitude) { return [ 'lat' => (float) $this->latitude, 'lng' => (float) $this->longitude ]; } return null; } // Jours de visite formatés public function getVisitingDaysFormattedAttribute() { if (!$this->visiting_days) { return 'Tous les jours'; } $days = [ 'monday' => 'Lundi', 'tuesday' => 'Mardi', 'wednesday' => 'Mercredi', 'thursday' => 'Jeudi', 'friday' => 'Vendredi', 'saturday' => 'Samedi', 'sunday' => 'Dimanche' ]; $visitDays = []; foreach ($this->visiting_days as $day) { $visitDays[] = $days[$day] ?? $day; } return implode(', ', $visitDays); } // Heures d'ouverture formatées public function getBusinessHoursAttribute() { if (!$this->opening_time || !$this->closing_time) { return null; } $hours = "{$this->opening_time->format('H:i')} - {$this->closing_time->format('H:i')}"; if ($this->break_start_time && $this->break_end_time) { $hours .= " (Pause: {$this->break_start_time->format('H:i')} - {$this->break_end_time->format('H:i')})"; } return $hours; } // Statut de crédit public function getCreditStatusAttribute() { if ($this->payment_terms === 0) { return 'Comptant'; } return "Crédit {$this->payment_terms} jours"; } /** * Méthodes utilitaires */ // Vérifier si le client peut être visité public function canBeVisited(): bool { return $this->is_active && !$this->do_not_visit; } // Vérifier si c'est un jour de visite public function isVisitDay($day = null): bool { if (!$this->visiting_days || count($this->visiting_days) === 0) { return true; // Peut être visité tous les jours } $day = $day ?? strtolower(now()->format('l')); return in_array($day, $this->visiting_days); } // Vérifier si c'est l'heure de visite public function isVisitTime($time = null): bool { if (!$this->opening_time || !$this->closing_time) { return true; // Pas d'heures définies } $time = $time ?? now(); $currentTime = $time->format('H:i:s'); // Vérifier si c'est pendant les heures d'ouverture if ($currentTime < $this->opening_time || $currentTime > $this->closing_time) { return false; } // Vérifier si c'est pendant la pause if ($this->break_start_time && $this->break_end_time) { if ($currentTime >= $this->break_start_time && $currentTime <= $this->break_end_time) { return false; } } return true; } // Calculer la distance depuis une position public function distanceFrom($latitude, $longitude): float { if (!$this->latitude || !$this->longitude) { return 0; } $earthRadius = 6371; // km $latDiff = deg2rad($latitude - $this->latitude); $lonDiff = deg2rad($longitude - $this->longitude); $a = sin($latDiff / 2) * sin($latDiff / 2) + cos(deg2rad($this->latitude)) * cos(deg2rad($latitude)) * sin($lonDiff / 2) * sin($lonDiff / 2); $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); return round($earthRadius * $c, 2); } // Obtenir les statistiques de visite public function getVisitStats($startDate = null, $endDate = null) { $query = $this->visits()->completed(); if ($startDate) { $query->where('visit_date', '>=', $startDate); } if ($endDate) { $query->where('visit_date', '<=', $endDate); } return [ 'total_visits' => $query->count(), 'last_visit' => $this->last_visit_date, 'next_visit' => $this->next_visit_date, 'average_duration' => $query->avg('duration_minutes'), ]; } // Obtenir les statistiques de commandes public function getOrderStats($startDate = null, $endDate = null) { $query = $this->orders(); if ($startDate) { $query->where('order_date', '>=', $startDate); } if ($endDate) { $query->where('order_date', '<=', $endDate); } return [ 'total_orders' => $query->count(), 'total_amount' => $query->sum('total_amount'), 'average_order' => $query->avg('total_amount'), 'pending_amount' => $query->where('payment_status', 'pending')->sum('total_amount'), ]; } // Mettre à jour la date de prochaine visite public function updateNextVisitDate(): void { // Logique basée sur la catégorie et l'historique $days = match($this->category) { 'A' => 7, // Visite hebdomadaire 'B' => 14, // Visite bi-mensuelle 'C' => 30, // Visite mensuelle default => 21 }; $this->update([ 'next_visit_date' => now()->addDays($days) ]); } /** * Méthodes pour l'API */ public function toArray() { $array = parent::toArray(); // Ajouter les attributs calculés $array['type_formatted'] = $this->type_formatted; $array['display_name'] = $this->display_name; $array['full_address'] = $this->full_address; $array['coordinates'] = $this->coordinates; $array['credit_status'] = $this->credit_status; $array['can_be_visited'] = $this->canBeVisited(); // Ajouter les relations si chargées if ($this->relationLoaded('city')) { $array['city_name'] = $this->city->name; } if ($this->relationLoaded('assignedVisitor')) { $array['visitor_name'] = $this->assignedVisitor->name; } return $array; } } ----- ./Listing.php ----- belongsTo(City::class); } public function visits() { return $this->hasMany(Visit::class); } public function actionRequests() { return $this->hasMany(ActionRequest::class); } public function appointments() { return $this->hasMany(Appointment::class); } } ----- ./Budget.php ----- belongsTo(User::class); } public function cycle() { return $this->belongsTo(Cycle::class); } } ----- ./WeeklyProgram.php ----- belongsTo(Cycle::class); } public function user() { return $this->belongsTo(User::class); } public function city() { return $this->belongsTo(City::class); } } ----- ./SalarySetting.php ----- hasMany(Visit::class); } } ----- ./WorkSession.php ----- 'datetime', 'ended_at' => 'datetime' ]; } ----- ./Location.php ----- 'datetime' ]; public function user() { return $this->belongsTo(User::class); } } ----- ./DelegueSalary.php ----- belongsTo(User::class); } } ----- ./VisitProduct.php ----- belongsTo(Visit::class); } public function product() { return $this->belongsTo(Product::class); } } ----- ./Expense.php ----- belongsTo(User::class); } } ----- ./User.php ----- 'datetime', 'birth_date' => 'date', 'hire_date' => 'date', 'last_login_at' => 'datetime', 'locked_until' => 'datetime', 'password_changed_at' => 'datetime', 'last_location_updated_at' => 'datetime', 'settings' => 'array', 'is_active' => 'boolean', 'must_change_password' => 'boolean', 'monthly_target' => 'decimal:2', 'commission_rate' => 'decimal:2', 'last_location_latitude' => 'decimal:8', 'last_location_longitude' => 'decimal:8', ]; protected static function boot() { parent::boot(); static::creating(function ($user) { if (empty($user->code)) { $user->code = $user->generateUniqueCode(); } if (!empty($user->password) && !Hash::isHashed($user->password)) { $user->password = Hash::make($user->password); } }); static::updating(function ($user) { if ($user->isDirty('password') && !Hash::isHashed($user->password)) { $user->password = Hash::make($user->password); $user->password_changed_at = now(); } }); } protected function generateUniqueCode(): string { $prefix = match($this->role) { 'admin' => 'DEL1', 'manager' => 'DEL2', 'supervisor' => 'DEL3', 'visitor' => 'DEL4', default => 'DEL5', }; do { $number = str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT); $code = $prefix . $number; } while (self::where('code', $code)->exists()); return $code; } // 👥 Un délégué a plusieurs visites public function visits() { return $this->hasMany(Visit::class); } // 🗺️ Relation N:N avec les villes autorisées public function cities() { return $this->belongsToMany(City::class, 'city_user'); } // 🧾 Notes de frais public function expenses() { return $this->hasMany(Expense::class); } // 🕒 Déplacements public function deplacements() { return $this->hasMany(Deplacement::class); } // ⏰ Permissions public function permissions() { return $this->hasMany(Permission::class); } // 🧩 Objectifs public function objectives() { return $this->hasMany(Objective::class); } // 💼 Budgets promotionnels public function budgets() { return $this->hasMany(Budget::class); } // 🧱 Actions promotionnelles public function actionRequests() { return $this->hasMany(ActionRequest::class); } // 📅 Rendez-vous public function appointments() { return $this->hasMany(Appointment::class); } // JWT methods public function getJWTIdentifier() { return $this->getKey(); } public function getJWTCustomClaims() { return []; } public function locations() { return $this->hasMany(Location::class); } } ----- ./City.php ----- belongsToMany(User::class, 'city_user', 'city_id', 'user_id'); // ou si c’est un lien direct One-to-Many : // return $this->hasMany(User::class); } // 🏥 Une ville possède plusieurs listings public function listings() { return $this->hasMany(Listing::class); } } ----- ./ActionRequest.php ----- 'date', ]; protected $appends = ['date_formatted']; public function getDateFormattedAttribute() { return $this->date_demande->format('d M Y'); } public function user() { return $this->belongsTo(User::class); } }