===== STRUCTURE ===== . ├── Admin │   ├── CityController.php │   ├── DashboardController.php │   ├── DeplacementController.php │   ├── ListingController.php │   ├── PermissionController.php │   ├── SalaryController.php │   ├── TicketController.php │   ├── UserController.php │   └── VisitController.php ├── API │   ├── ActionRequestController.php │   ├── AppointmentController.php │   ├── AuthController.php │   ├── CityController.php │   ├── CycleController.php │   ├── DashboardController.php │   ├── DeplacementController.php │   ├── ExpenseController.php │   ├── ExpenseDashboardController.php │   ├── ListingController.php │   ├── LocationController.php │   ├── NotificationController.php │   ├── ObjectiveController.php │   ├── PermissionController.php │   ├── ProductController.php │   ├── UserController.php │   ├── VisitController.php │   ├── VisitInProgressController.php │   ├── VisitProductController.php │   ├── VisitTypeController.php │   └── WeeklyProgramController.php ├── Controller.php ├── ExpenseDashboardController.php ├── LocationController.php ├── output.txt └── WorkSessionController.php 3 directories, 35 files ===== CONTENU DES FICHIERS ===== ----- ./Admin/PermissionController.php ----- orderByDesc('start_date'); if ($search = $request->get('search')) { $query->whereHas('user', fn($q)=>$q->where('name','like',"%$search%")); } if ($statut = $request->get('status')) { $query->where('status',$statut); } if ($reason = $request->get('reason')) { $query->where('reason',$reason); } if ($from = $request->get('from') and $to = $request->get('to')) { $query->whereBetween('start_date', [$from,$to]); } $permissions = $query->paginate(10)->withQueryString(); return view('pages.permissions.index', compact('permissions')); } public function show(Permission $permission) { $permission->load('user'); return view('pages.permissions.show', compact('permission')); } public function updateStatus(Request $request, Permission $permission) { $validated = $request->validate(['status'=>'required|in:accepté,en_cours,refus']); $permission->update(['status'=>$validated['status']]); return back()->with('success','Statut mis à jour.'); } } ----- ./Admin/DashboardController.php ----- count(); $totalVisits = Visit::count(); $validVisits = Visit::where('status', 'valide')->count(); $totalDeplacements = Deplacement::count(); $deplacementsEnCours = Deplacement::where('status', 'en_cours')->count(); $totalPermissions = Permission::count(); $permissionsPending = Permission::where('status', 'en_cours')->count(); // ========================= // VISITES PAR STATUT // ========================= $visitesByStatus = Visit::select('status', DB::raw('COUNT(*) as count')) ->groupBy('status') ->pluck('count', 'status') ->toArray(); // Garantir les clés $allStatuses = ['valide', 'invalide', 'double', 'non_realisee']; foreach ($allStatuses as $status) { if (!isset($visitesByStatus[$status])) { $visitesByStatus[$status] = 0; } } // ========================= // TOP DÉLÉGUÉS // ========================= $topDelegues = User::select('users.id', 'users.name', DB::raw('COUNT(visits.id) as total')) ->join('visits', 'users.id', '=', 'visits.user_id') ->where('visits.status', 'valide') ->groupBy('users.id', 'users.name') ->orderByDesc('total') ->take(5) ->get(); // ========================= // ÉVOLUTION SUR 12 MOIS // ========================= $rawVisitsPerMonth = Visit::select( DB::raw("YEAR(date_visit) as year"), DB::raw("MONTH(date_visit) as month"), DB::raw("COUNT(*) as total") ) ->where('date_visit', '>=', $now->copy()->subMonths(11)->startOfMonth()) ->groupBy('year', 'month') ->orderBy('year') ->orderBy('month') ->get() ->keyBy(function ($item) { return $item->year . '-' . str_pad($item->month, 2, '0', STR_PAD_LEFT); }); $visitsPerMonth = collect(); for ($i = 11; $i >= 0; $i--) { $date = $now->copy()->subMonths($i); $key = $date->format('Y-m'); $visitsPerMonth->push([ 'month' => $date->translatedFormat('M'), 'total' => $rawVisitsPerMonth[$key]->total ?? 0, ]); } // ========================= // ACTIVITÉS RÉCENTES // ========================= $recentVisits = Visit::with('user') ->latest('date_visit') ->take(5) ->get(); // ========================= // TAUX // ========================= $validVisitsRate = $totalVisits > 0 ? round(($validVisits / $totalVisits) * 100, 1) : 0; $activeUsersRate = $totalUsers > 0 ? round(($activeUsers / $totalUsers) * 100, 1) : 0; return view('pages.dashboard.admin-dashboard', compact( 'totalUsers', 'activeUsers', 'totalVisits', 'validVisits', 'totalDeplacements', 'deplacementsEnCours', 'totalPermissions', 'permissionsPending', 'visitesByStatus', 'topDelegues', 'visitsPerMonth', 'recentVisits', 'validVisitsRate', 'activeUsersRate' )); } } ----- ./Admin/ListingController.php ----- with('city.users'); if ($request->search) { $query->where(function($q) use ($request) { $q->where('name', 'like', '%' . $request->search . '%') ->orWhere('address', 'like', '%' . $request->search . '%'); }); } if ($request->type) $query->where('type', $request->type); if ($request->specialty) $query->where('specialty', $request->specialty); if ($request->city) $query->where('city_id', $request->city); if ($request->delegate) { $query->whereHas('city.users', fn($q) => $q->where('name', $request->delegate)); } if ($request->sort) { if ($request->sort === 'new') $query->orderBy('created_at','desc'); elseif ($request->sort === 'name') $query->orderBy('name'); elseif ($request->sort === 'type') $query->orderBy('type'); elseif ($request->sort === 'specialty') $query->orderBy('specialty'); // Tri sur delegate non direct, ignore ici } $perPage = $request->per_page ?: 10; $listings = $query->paginate($perPage)->withQueryString(); return view('pages.listings.index', [ 'listings' => $listings, 'cities' => City::all(), 'delegates' => User::all(), 'types' => self::TYPES, 'specialties' => self::SPECIALTIES ]); } /** Formulaire création */ public function create() { return view('pages.listings.create', [ 'cities' => City::all(), 'types' => self::TYPES, 'specialties' => self::SPECIALTIES ]); } /** Enregistrer nouveau listing */ public function store(Request $request) { $validated = $request->validate([ 'name' => 'required|string|max:255', 'type' => 'required|string|max:50', 'specialty' => 'nullable|string|max:100', 'address' => 'nullable|string|max:255', 'city_id' => 'nullable|exists:cities,id', 'lat' => 'nullable|numeric', 'lng' => 'nullable|numeric', ]); Listing::create($validated); return redirect()->route('admin.listings.index')->with('success', 'Listing ajouté avec succès.'); } /** Modifier un listing */ public function edit(Listing $listing) { return view('pages.listings.edit', [ 'listing' => $listing, 'cities' => City::all(), 'types' => self::TYPES, 'specialties' => self::SPECIALTIES ]); } /** Mettre à jour un listing */ public function update(Request $request, Listing $listing) { $validated = $request->validate([ 'name' => 'required|string|max:255', 'type' => 'required|string|max:50', 'specialty' => 'nullable|string|max:100', 'address' => 'nullable|string|max:255', 'city_id' => 'nullable|exists:cities,id', 'lat' => 'nullable|numeric', 'lng' => 'nullable|numeric', ]); $listing->update($validated); return redirect()->route('admin.listings.index')->with('success', 'Listing mis à jour avec succès.'); } /** Supprimer un listing */ public function destroy(Listing $listing) { $listing->delete(); return back()->with('success','Listing supprimé avec succès.'); } } ----- ./Admin/DeplacementController.php ----- get('search')) { $query->where(function($q) use ($search) { $q->where('from_city','like',"%$search%") ->orWhere('to_city','like',"%$search%"); }); } if ($status = $request->get('status')) { $query->where('status', $status); } if ($from = $request->get('from') and $to = $request->get('to')) { $query->whereBetween('date_depart', [$from, $to]); } $deplacements = $query->orderBy('date_depart','desc')->paginate(10)->withQueryString(); return view('pages.deplacements.index', compact('deplacements')); } /** ➕ Formulaire création */ public function create() { $users = User::orderBy('name')->get(); $cities = City::orderBy('name')->get(); return view('pages.deplacements.create', compact('users','cities')); } /** 💾 Sauvegarde */ public function store(Request $request) { $validated = $request->validate([ 'user_id' => 'required|exists:users,id', 'from_city' => 'required|string|max:100', 'to_city' => 'required|string|max:100', 'distance_km' => 'nullable|numeric|min:0', 'date_depart' => 'required|date', 'status' => 'required|in:realise,non_realise', ]); Deplacement::create($validated); return redirect()->route('admin.deplacements.index')->with('success','Déplacement ajouté avec succès.'); } /** ✏️ Modifier */ public function edit(Deplacement $deplacement) { $users = User::orderBy('name')->get(); $cities = City::orderBy('name')->get(); return view('pages.deplacements.edit', compact('deplacement','users','cities')); } /** 🔄 Update */ public function update(Request $request, Deplacement $deplacement) { $validated = $request->validate([ 'user_id' => 'required|exists:users,id', 'from_city' => 'required|string|max:100', 'to_city' => 'required|string|max:100', 'distance_km' => 'nullable|numeric|min:0', 'date_depart' => 'required|date', 'status' => 'required|in:realise,non_realise', ]); $deplacement->update($validated); return redirect()->route('admin.deplacements.index')->with('success','Déplacement mis à jour.'); } /** ❌ Supprimer */ public function destroy(Deplacement $deplacement) { $deplacement->delete(); return back()->with('success','Déplacement supprimé.'); } /** 📄 Détails imprimables */ public function show(Deplacement $deplacement) { $deplacement->load('user'); return view('pages.deplacements.show', compact('deplacement')); } } ----- ./Admin/TicketController.php ----- where('has_ticket', true); if($search = $request->get('search')) { $query->whereHas('listing', fn($q) => $q->where('name','like',"%$search%")) ->orWhereHas('user', fn($q) => $q->where('name','like',"%$search%")); } $visits = $query->orderBy('date_visit','desc')->paginate(15); return view('pages.tickets.index', compact('visits')); } /** Détail d’une visite */ public function show(Visit $visit) { $visit->load(['user','listing','userAccompagnant']); return view('pages.tickets.show', compact('visit')); } /** Admin corrige les coordonnées et statut */ public function update(Request $request, Visit $visit) { $validated = $request->validate([ 'poslat' => 'nullable|numeric', 'poslong' => 'nullable|numeric', 'status' => 'nullable|in:valide,invalide', ]); if(isset($validated['poslat']) && isset($validated['poslong'])) { // mettre nouvelles coordonnées et désactiver le ticket $visit->poslat = $validated['poslat']; $visit->poslong = $validated['poslong']; $visit->has_ticket = false; } if(isset($validated['status'])) { $visit->status = $validated['status']; } $visit->save(); return redirect()->back()->with('success','Visite mise à jour avec succès.'); } } ----- ./Admin/CityController.php ----- orderBy('name'); if ($region = $request->get('region')) { $query->where('region', $region); } $cities = $query->paginate(10)->withQueryString(); // toutes les régions distinctes $regions = City::select('region')->distinct()->pluck('region'); return view('pages.cities.index', compact('cities', 'regions')); } /** Formulaire création */ public function create() { $regions = City::select('region')->distinct()->pluck('region'); return view('pages.cities.create', compact('regions')); } /** Création */ public function store(Request $request) { $validated = $request->validate([ 'name' => 'required|string|max:255|unique:cities,name', 'region' => 'required|string|max:255', ]); City::create($validated); return redirect()->route('admin.cities.index')->with('success', 'Secteur ajouté avec succès.'); } /** Formulaire modification */ public function edit(City $city) { $regions = City::select('region')->distinct()->pluck('region'); return view('pages.cities.edit', compact('city', 'regions')); } /** Sauvegarde modification */ public function update(Request $request, City $city) { $validated = $request->validate([ 'name' => "required|string|max:255|unique:cities,name,{$city->id}", 'region' => 'required|string|max:255', ]); $city->update($validated); return redirect()->route('admin.cities.index')->with('success', 'Secteur mis à jour avec succès.'); } /** Supprimer */ public function destroy(City $city) { $city->delete(); return back()->with('success','Secteur supprimé.'); } /** Attacher un ou plusieurs délégués à un secteur */ public function attachDelegues(Request $request, City $city) { $validated = $request->validate([ 'users' => 'array|required', 'users.*' => 'exists:users,id', ]); $city->users()->sync($validated['users']); return back()->with('success', 'Les délégués ont été mis à jour pour ce secteur.'); } } ----- ./Admin/VisitController.php ----- get('search')) { $query->whereHas('listing', fn($q) => $q->where('name', 'like', "%$search%")) ->orWhereHas('user', fn($q) => $q->where('name', 'like', "%$search%")); } // Filtre type de visite if ($type = $request->get('visite_type')) { $query->where('visite_type', $type); } // Filtre statut if ($status = $request->get('status')) { $query->where('status', $status); } // Filtre délégué if ($user_id = $request->get('user_id')) { $query->where('user_id', $user_id); } // Filtre ville many-to-many via table pivot city_user if ($city_id = $request->get('city_id')) { $query->whereHas('user.cities', fn($q) => $q->where('cities.id', $city_id)); } // Filtre plage de date if ($from = $request->get('from') and $to = $request->get('to')) { $query->whereBetween('date_visit', [$from, $to]); } // Pagination $visits = $query->orderBy('date_visit','desc')->paginate(15)->withQueryString(); // Pour les selects $users = User::orderBy('name')->get(); $cities = City::orderBy('name')->get(); return view('pages.visits.index', compact('visits','users','cities')); } public function exportXls(Request $request) { // On récupère les mêmes filtres que dans index() $filters = [ 'search' => $request->get('search'), 'visite_type' => $request->get('visite_type'), 'status' => $request->get('status'), 'from' => $request->get('from'), 'to' => $request->get('to'), ]; // On envoie les filtres à l’export return Excel::download(new VisitsExport($filters), 'visites_filtrees.xlsx'); } /** Page détail */ public function show(Visit $visit) { $visit->load(['user','listing']); return view('pages.visits.show', compact('visit')); } /** Valider / invalider */ public function updateStatus(Request $request, Visit $visit) { $validated = $request->validate([ 'status' => 'required|in:valide,invalide', ]); $visit->update(['status' => $validated['status']]); return back()->with('success', 'Statut de la visite mis à jour.'); } public function progress(Request $request) { $query = VisitInProgress::with([ 'user', 'listing.city' ])->where('status', 'in_progress'); // 🔥 always in progress // 🔍 Recherche (délégué ou client) if ($request->filled('search')) { $search = $request->search; $query->where(function ($q) use ($search) { $q->whereHas('user', function ($q) use ($search) { $q->where('name', 'like', "%$search%"); })->orWhereHas('listing', function ($q) use ($search) { $q->where('name', 'like', "%$search%"); }); }); } // 👤 Filtre user if ($request->filled('user_id')) { $query->where('user_id', $request->user_id); } // 📍 Filtre secteur if ($request->filled('city_id')) { $query->whereHas('listing.city', function ($q) use ($request) { $q->where('id', $request->city_id); }); } // 📅 Date début if ($request->filled('from')) { $query->whereDate('start_time', '>=', $request->from); } if ($request->filled('to')) { $query->whereDate('start_time', '<=', $request->to); } // 📦 Résultat $visits = $query->latest()->paginate(15); // filtres UI $users = \App\Models\User::all(); $cities = \App\Models\City::all(); return view('pages.visits.progress', compact('visits', 'users', 'cities')); } } ----- ./Admin/UserController.php ----- get('search')) { $query->where(function ($q) use ($search) { $q->where('name', 'like', "%$search%") ->orWhere('email', 'like', "%$search%") ->orWhere('code', 'like', "%$search%"); }); } if ($role = $request->get('role')) { $query->where('role', $role); } if ($status = $request->get('status')) { $query->where('is_active', $status === 'active'); } $users = $query->orderBy('name')->paginate(10)->withQueryString(); return view('pages.users.index', compact('users')); } /** 📄 Afficher détail A4 */ public function show(User $user) { return view('pages.users.show', compact('user')); } public function create() { return view('pages.users.create'); } public function store(Request $request) { $validated = $request->validate([ 'name' => 'required|string|max:255', 'email' => 'required|email|unique:users,email', 'password' => 'required|string|min:6|confirmed', 'role' => 'required|in:admin,manager,delegue,visitor', 'phone' => 'nullable|string|max:20', 'mobile' => 'nullable|string|max:20', 'address' => 'nullable|string|max:255', 'department' => 'nullable|string|max:50', 'hire_date' => 'nullable|date', 'contract_type' => 'nullable|in:permanent,temporary,freelance', 'is_active' => 'nullable|boolean', ]); $validated['password'] = Hash::make($validated['password']); $user = User::create($validated); return redirect()->route('admin.users.index') ->with('success', "L’utilisateur {$user->name} a bien été ajouté !"); } public function edit(User $user) { return view('pages.users.edit', compact('user')); } public function update(Request $request, User $user) { $validated = $request->validate([ 'name' => 'required|string|max:255', 'email' => 'required|email|unique:users,email,' . $user->id, 'role' => 'required|in:admin,manager,delegue,visitor', 'phone' => 'nullable|string|max:20', 'mobile' => 'nullable|string|max:20', 'address' => 'nullable|string|max:255', 'department' => 'nullable|string|max:50', 'hire_date' => 'nullable|date', 'contract_type' => 'nullable|in:permanent,temporary,freelance', 'is_active' => 'nullable|boolean', ]); // Mise à jour simple $user->update($validated); return redirect()->route('admin.users.index') ->with('success', "L’utilisateur {$user->name} a été mis à jour avec succès !"); }public function tracking(Request $request, User $user) { /* |-------------------------------------------------------------------------- | FILTRE GLOBAL (DEFAULT = TODAY) |-------------------------------------------------------------------------- */ $period = $request->period ?? 'today'; $start = null; $end = null; switch ($period) { case 'today': $start = Carbon::today(); $end = Carbon::today()->endOfDay(); break; case 'yesterday': $start = Carbon::yesterday(); $end = Carbon::yesterday()->endOfDay(); break; case 'before_yesterday': $start = Carbon::today()->subDays(2); $end = Carbon::today()->subDays(2)->endOfDay(); break; case 'this_week': $start = Carbon::now()->startOfWeek(); $end = Carbon::now()->endOfWeek(); break; case 'all': $start = null; $end = null; break; } /* |-------------------------------------------------------------------------- | FILTRE PERSONNALISÉ PRIORITAIRE |-------------------------------------------------------------------------- */ if ($request->start && $request->end) { $start = Carbon::parse($request->start); $end = Carbon::parse($request->end); } /* |-------------------------------------------------------------------------- | LOCATIONS |-------------------------------------------------------------------------- */ $locationsQuery = $user->locations()->orderBy('recorded_at'); if ($start && $end) { $locationsQuery->whereBetween('recorded_at', [$start, $end]); } $locations = $locationsQuery->get(); /* |-------------------------------------------------------------------------- | VISITS (FILTRÉES AUSSI) |-------------------------------------------------------------------------- */ $visitsQuery = Visit::with(['listing', 'cycle']) ->where('user_id', $user->id); if ($start && $end) { $visitsQuery->whereBetween('date_visit', [$start, $end]); } $visits = $visitsQuery->latest()->get(); /* |-------------------------------------------------------------------------- | CALCUL ARRÊTS (> 5 MIN) |-------------------------------------------------------------------------- */ $disconnects = []; for ($i = 1; $i < count($locations); $i++) { $prev = $locations[$i - 1]->recorded_at; $current = $locations[$i]->recorded_at; $diffInSeconds = $prev->diffInSeconds($current); if ($diffInSeconds > 300) { $duration = Carbon::createFromTimestamp($diffInSeconds)->utc(); $disconnects[] = [ 'from' => $prev, 'to' => $current, 'duration' => $duration->format('j\j H\h i\m s\s'), 'duration_seconds' => $diffInSeconds // utile si tu veux stats plus tard ]; } } /* |-------------------------------------------------------------------------- | STATS |-------------------------------------------------------------------------- */ $startTime = optional($locations->first())->recorded_at; $endTime = optional($locations->last())->recorded_at; $durationFormatted = null; if ($startTime && $endTime) { $durationFormatted = $startTime->diff($endTime)->format('%h h %i min'); } /* |-------------------------------------------------------------------------- | KPI VISITES |-------------------------------------------------------------------------- */ $kpiByListingType = $visits ->filter(fn($v) => $v->listing) ->groupBy(fn($v) => $v->listing->type); /* |-------------------------------------------------------------------------- | RETURN |-------------------------------------------------------------------------- */ return view('pages.users.tracking', compact( 'user', 'locations', 'disconnects', 'startTime', 'endTime', 'durationFormatted', 'visits', 'kpiByListingType', 'period', 'start', 'end' )); } } ----- ./Admin/SalaryController.php ----- get(); $currentCycle = $request->get('cycle_id', $cycles->first()?->id ?? null); $setting = SalarySetting::first(); $primePerVisit = $setting?->prime_per_visit ?? 10; $delegues = User::all(); $salaires = $delegues->map(function ($del) use ($primePerVisit, $currentCycle) { $fixe = DelegueSalary::where('user_id',$del->id)->value('base_salary') ?? 0; $visites = Visit::where('user_id',$del->id) ->where('status','valide') ->when($currentCycle, fn($q)=>$q->where('cycle_id',$currentCycle)) ->count(); $prime = $visites * $primePerVisit; return [ 'delegue'=>$del, 'fixe'=>$fixe, 'visites'=>$visites, 'prime'=>$prime, 'total'=>$fixe+$prime, ]; }); return view('pages.salaries.index', compact('cycles','currentCycle','salaires','primePerVisit')); } /** Modifier salaire fixe délégué */ public function updateFixed(Request $request, User $user) { $validated = $request->validate(['base_salary'=>'required|numeric|min:0']); DelegueSalary::updateOrCreate( ['user_id'=>$user->id], ['base_salary'=>$validated['base_salary']] ); return back()->with('success','Salaire fixe modifié.'); } /** Modifier la prime globale par visite */ public function updatePrime(Request $request) { $validated = $request->validate(['prime_per_visit'=>'required|numeric|min:0']); SalarySetting::updateOrCreate(['id'=>1],['prime_per_visit'=>$validated['prime_per_visit']]); return back()->with('success','Prime par visite mise à jour.'); } /** Détail d’un délégué */ public function show(User $user, Request $request) { $cycles = Cycle::orderByDesc('start_date')->get(); $currentCycle = $request->get('cycle_id', $cycles->first()?->id ?? null); $setting = SalarySetting::first(); $primePerVisit = $setting?->prime_per_visit ?? 10; $fixe = DelegueSalary::where('user_id',$user->id)->value('base_salary') ?? 0; $visites = Visit::where('user_id',$user->id) ->where('status','valide') ->when($currentCycle, fn($q)=>$q->where('cycle_id',$currentCycle)) ->get(); $prime = $visites->count() * $primePerVisit; $total = $fixe + $prime; return view('pages.salaries.show', compact('user','cycles','currentCycle','fixe','visites','prime','total','primePerVisit')); } } ----- ./API/ProductController.php ----- json(Product::all()); } } ----- ./API/ExpenseController.php ----- json( Expense::where('user_id', $request->user()->id)->get() ); } public function store(Request $request) { $validated = $request->validate([ 'category' => 'required|string', 'amount' => 'required|numeric', 'description' => 'nullable|string', ]); $expense = Expense::create([ 'user_id' => $request->user()->id, ...$validated ]); return response()->json(['message' => 'Dépense enregistrée', 'data' => $expense]); } } ----- ./API/AuthController.php ----- all(), [ 'name' => 'required|string|max:255', 'email' => 'required|string|email|max:255|unique:users', 'password' => 'required|string|min:6|confirmed', ]); if ($validator->fails()) { return response()->json(['errors' => $validator->errors()], 422); } $user = User::create([ 'name' => $request->name, 'email' => $request->email, 'password' => bcrypt($request->password), ]); return response()->json(['message' => 'User registered successfully', 'user' => $user], 201); } // Login user and return JWT token public function login(Request $request) { $credentials = $request->only('email', 'password'); if (!$token = auth('api')->attempt($credentials)) { return response()->json(['error' => 'Unauthorized'], 401); } return $this->respondWithToken($token); } // Get user profile public function profile() { return response()->json(auth('api')->user()); } // Logout user (invalidate token) public function logout() { auth('api')->logout(); return response()->json(['message' => 'Successfully logged out']); } // Refresh JWT token public function refresh() { return $this->respondWithToken(auth('api')->refresh()); } // Return token response structure protected function respondWithToken($token) { return response()->json([ 'access_token' => $token, 'token_type' => 'bearer', 'expires_in' => auth('api')->factory()->getTTL() * 60, ]); } } ----- ./API/PermissionController.php ----- id()) ->orderByDesc('created_at') ->get(); return response()->json([ 'status' => 'success', 'data' => $permissions ]); } /** * ✅ Créer une nouvelle permission */ public function store(Request $request) { $validated = $request->validate([ 'start_date' => 'required|date', 'end_date' => 'nullable|date|after_or_equal:start_date', 'half_day' => 'boolean', 'period' => 'nullable|in:matin,apres_midi', 'reason' => 'required|in:trajet,reunion,congres,formation,maladie,affaire_personnel', 'notes' => 'nullable|string', ]); $permission = Permission::create([ 'user_id' => auth()->id(), 'start_date' => $validated['start_date'], 'end_date' => $validated['end_date'] ?? null, 'half_day' => $validated['half_day'] ?? false, 'period' => $validated['period'] ?? null, 'reason' => $validated['reason'], 'notes' => $validated['notes'] ?? null, 'status' => 'en_cours', ]); return response()->json([ 'status' => 'success', 'message' => 'Demande envoyée avec succès', 'data' => $permission ], 201); } } ----- ./API/VisitProductController.php ----- user()->id) ->orderByDesc('created_at') ->get(); } // 🔴 Nombre non lues public function unreadCount(Request $request) { return response()->json([ 'count' => Notification::where('user_id', $request->user()->id) ->where('is_read', false) ->count() ]); } // ✅ Marquer une notification comme lue public function markAsRead($id, Request $request) { Notification::where('id', $id) ->where('user_id', $request->user()->id) ->update(['is_read' => true]); return response()->json(['success' => true]); } // ✅ Marquer toutes comme lues public function markAllAsRead(Request $request) { Notification::where('user_id', $request->user()->id) ->where('is_read', false) ->update(['is_read' => true]); return response()->json(['success' => true]); } } ----- ./API/DashboardController.php ----- user(); if (!$user) { return response()->json([ 'error' => 'Utilisateur non authentifié', 'code' => 401 ], 401); } $now = Carbon::now(); // 🔁 Cycle actuel $cycle = Cycle::whereDate('start_date', '<=', $now) ->whereDate('end_date', '>=', $now) ->first(); if (!$cycle) { return response()->json([ 'error' => 'Aucun cycle actif trouvé', 'code' => 404 ], 404); } $cycleId = $cycle->id; // 🎯 Objectif = nombre de listings $cities = $user->cities()->pluck('city_id'); if ($cities->isEmpty()) { return response()->json([ 'error' => 'Aucune ville associée à cet utilisateur', 'code' => 422 ], 422); } $objectif = Listing::whereIn('city_id', $cities)->count(); if ($objectif === 0) { return response()->json([ 'error' => 'Aucun listing trouvé pour les villes de l’utilisateur', 'code' => 404 ], 404); } // ✅ Visites du cycle actuel $visitsInCycle = Visit::where('user_id', $user->id) ->where('cycle_id', $cycleId) ->count(); // 💰 Gains (visites validées × 10 DH) $validatedVisits = Visit::where('user_id', $user->id) ->where('cycle_id', $cycleId) ->where('status', 'valide') ->count(); $gains = $validatedVisits * self::PRIME_PER_VISIT; // 📊 Statistiques (tous les types) $listingTypes = [ 'PRIVE','PUBLIC','EMI-PUBLIC','MILITAIRE', 'PHARMACIE','GROSSISTERIE','MATERNITE','URGENCE' ]; $stats = collect($listingTypes)->mapWithKeys(function ($type) use ($user, $cycleId) { $count = Visit::where('user_id', $user->id) ->where('cycle_id', $cycleId) ->whereHas('listing', fn($q) => $q->where('type', $type)) ->count(); return [$type => $count]; }); // 🕒 Activité récente (visites + déplacements) $recentVisits = Visit::with('listing') ->where('user_id', $user->id) ->select('id', 'listing_id', 'status', 'created_at') ->latest() ->take(5) ->get() ->map(fn($v) => [ 'type' => 'visit', 'title' => $v->listing?->name ?? 'Listing supprimé', 'subtitle' => match ($v->status) { 'valide' => 'Visite validée', 'invalide' => 'Visite refusée', default => 'Visite en attente', }, 'status' => $v->status === 'valide' ? 'success' : 'warning', 'date' => $v->created_at, ]); $recentTrips = Deplacement::where('user_id', $user->id) ->select('id', 'from_city', 'to_city', 'status', 'created_at') ->latest() ->take(5) ->get() ->map(fn($d) => [ 'type' => 'deplacement', 'title' => "{$d->from_city} → {$d->to_city}", 'subtitle' => 'Déplacement ' . str_replace('_', ' ', $d->status), 'status' => $d->status === 'realise' ? 'success' : 'warning', 'date' => $d->created_at, ]); $recentActivity = $recentVisits ->merge($recentTrips) ->sortByDesc('date') ->take(5) ->values(); // ✅ Response return response()->json([ 'user' => [ 'name' => $user->name, 'date' => $now->translatedFormat('d F Y'), 'cities' => $cities, ], 'kpis' => [ 'visits' => $visitsInCycle, 'objectif' => $objectif, 'gains' => $gains . ' DH', ], 'statistics' => $stats, 'recent_activity' => $recentActivity, ]); } catch (Exception $e) { return response()->json([ 'error' => 'Une erreur est survenue : ' . $e->getMessage(), 'code' => 500 ], 500); } } } ----- ./API/ListingController.php ----- user()->cities->pluck('id'); // Récupérer le cycle actuel $today = Carbon::today(); $currentCycle = Cycle::where('start_date', '<=', $today) ->where('end_date', '>=', $today) ->first(); $listings = Listing::with('city') ->whereIn('city_id', $cityIds) ->get(); // Ajouter hasExist + trier $listings = $listings->map(function ($listing) use ($currentCycle) { $listing->cycle_name = $currentCycle?->name; $listing->hasExist = false; if ($currentCycle) { $listing->hasExist = Visit::where('listing_id', $listing->id) ->where('cycle_id', $currentCycle->id) ->exists(); } // Forcer false pour certains types if ($listing->type === 'MATERNITE' || $listing->type === 'URGENCE') { $listing->hasExist = false; } return $listing; }) ->sortBy([ ['hasExist', 'asc'], // false (0) avant true (1) ['created_at', 'desc'], // plus récent en premier ]) ->values(); return response()->json($listings); } public function show($id) { return response()->json(Listing::findOrFail($id)); } public function updateCoordinates(Request $request, $id) { $listing = Listing::findOrFail($id); // Valider les données $request->validate([ 'lat' => 'nullable|numeric|between:-90,90', 'lng' => 'nullable|numeric|between:-180,180', ]); // Mettre à jour uniquement lat et lng $listing->lat = $request->lat; $listing->lng = $request->lng; $listing->save(); return response()->json([ 'message' => 'Coordinates updated successfully', 'listing' => $listing ]); } public function store(Request $request) { $user = $request->user(); $validated = $request->validate([ 'name' => 'required|string|max:255', 'address' => 'required|string|max:255', 'city_id' => 'required|exists:cities,id', 'type' => 'nullable|string|max:100', 'specialty' => 'nullable|string|max:150', ]); // ✅ Vérifier que la ville appartient à l'utilisateur if (! $user->cities()->where('cities.id', $validated['city_id'])->exists()) { return response()->json([ 'message' => 'Ville non autorisée' ], 403); } $listing = Listing::create($validated); return response()->json([ 'message' => 'Listing créé avec succès', 'listing' => $listing ], 201); } public function update(Request $request, $id) { $listing = Listing::findOrFail($id); $validated = $request->validate([ 'name' => 'required|string|max:255', 'address' => 'required|string|max:255', 'type' => 'nullable|string|max:100', 'specialty' => 'nullable|string|max:150', ]); $listing->update($validated); return response()->json([ 'message' => 'Listing modifié avec succès', 'listing' => $listing ]); } } ----- ./API/DeplacementController.php ----- id()) ->orderBy('date_depart', 'desc') ->get(); return response()->json([ 'status' => 'success', 'data' => $deplacements ]); } /** * ✅ Mise à jour du statut * - non_realise → en_cours * - en_cours → realise * - en_cours → non_realise (annuler) */ public function updateStatus(Request $request, $id) { $request->validate([ 'status' => 'required|in:non_realise,en_cours,realise', ]); $deplacement = Deplacement::where('id', $id) ->where('user_id', auth()->id()) ->first(); if (!$deplacement) { return response()->json([ 'status' => 'error', 'message' => 'Déplacement non trouvé ou accès refusé' ], 404); } // ✅ Mise à jour $deplacement->status = $request->status; $deplacement->save(); return response()->json([ 'status' => 'success', 'message' => 'Statut mis à jour avec succès', 'data' => $deplacement ]); } } ----- ./API/VisitTypeController.php ----- user()->cities; return response()->json($cities); } // 🔹 Liste globale des villes (admin) public function index() { return response()->json(City::all()); } } ----- ./API/VisitController.php ----- user()->cities->pluck('id'); $visits = Visit::with(['user', 'listing', 'cycle']) ->whereHas('listing', function ($query) use ($cityIds) { $query->whereIn('city_id', $cityIds); }) ->latest() ->get(); return response()->json([ 'status' => 'success', 'data' => $visits ]); } public function valideVisits(Request $request) { $cityIds = $request->user()->cities->pluck('id'); $visits = Visit::with(['user', 'listing', 'cycle']) ->where('status', 'valide') ->whereHas('listing', function ($query) use ($cityIds) { $query->whereIn('city_id', $cityIds); }) ->latest() ->get(); return response()->json([ 'status' => 'success', 'data' => $visits ]); } public function invalideVisits() { $visits = Visit::with(['user', 'listing', 'cycle']) ->where('status', 'invalide') ->latest() ->get(); return response()->json([ 'status' => 'success', 'data' => $visits ]); } public function doubleVisits() { $visits = Visit::with(['user', 'listing', 'cycle']) ->where('status', 'double') ->latest() ->get(); return response()->json([ 'status' => 'success', 'data' => $visits ]); } public function nonRealiseeVisits() { $visits = Visit::with(['user', 'listing', 'cycle']) ->where('status', 'non_realisee') ->latest() ->get(); return response()->json([ 'status' => 'success', 'data' => $visits ]); } /** * Enregistrer une nouvelle visite */ public function store(Request $request) { // 1️⃣ Validation $validator = Validator::make($request->all(), [ 'listing_id' => 'required|integer|exists:listings,id', 'visite_type' => 'required|string|in:Face à face,Double,Accompagné', 'user_latitude' => 'nullable|numeric', 'user_longitude' => 'nullable|numeric', 'distance_meters' => 'nullable|numeric', 'admin_pending' => 'nullable|boolean', 'interlocuteur' => 'nullable|string|max:255', 'role' => 'nullable|string|max:255', 'rupture_stock' => 'nullable|boolean', 'marche_direct' => 'nullable|boolean', 'objection' => 'nullable|boolean', 'important' => 'nullable|boolean', 'elapsed_time' => 'nullable|integer|min:0', 'proposed_meds' => 'nullable|array', 'requested_meds' => 'nullable|array', 'results' => 'nullable|array', 'notes' => 'nullable|string', 'status' => 'nullable|in:valide,invalide,double,non_realisee,distante', 'user_accompagnant_id' => 'nullable|integer|exists:users,id', 'poslat' => 'nullable|numeric', 'poslong' => 'nullable|numeric', 'has_ticket' => 'nullable|boolean', 'is_far' => 'nullable|boolean', 'far_reason' => 'nullable|string|max:255', 'far_explanation' => 'nullable|string', // 'image' => 'nullable|string', ]); if ($validator->fails()) { return response()->json([ 'status' => 'error', 'message' => 'Validation failed', 'errors' => $validator->errors(), ], 422); } $data = $validator->validated(); $today = Carbon::today(); $thirtyMinutesAgo = Carbon::now()->subMinutes(30); $alreadyExists = Visit::where('user_id', auth()->id()) ->where('listing_id', $data['listing_id']) ->where('date_visit', '>=', $thirtyMinutesAgo) ->exists(); if ($alreadyExists) { return response()->json([ 'status' => 'error', 'message' => 'Une visite a déjà été enregistrée il y a quelque minutes' ], 422); } // 3️⃣ Assigner les valeurs calculées $data['status'] = 'valide'; if (!empty($request->user_position) && is_array($request->user_position)) { $data['poslat'] = $request->user_position['latitude'] ?? null; $data['poslong'] = $request->user_position['longitude'] ?? null; } // 4️⃣ Récupérer le cycle du jour $cycle = Cycle::where('start_date', '<=', $today) ->where('end_date', '>=', $today) ->first(); if (!$cycle) { return response()->json([ 'status' => 'error', 'message' => 'Aucun cycle trouvé pour aujourd’hui', ], 422); } $data['cycle_id'] = $cycle->id; $data['user_id'] = auth()->id(); $data['date_visit'] = now(); // 5️⃣ Gestion de l'image if (!empty($data['image']) && Str::startsWith($data['image'], 'data:image')) { $imageData = explode(',', $data['image'])[1]; $imageName = Str::uuid() . '.png'; Storage::disk('public')->put('visits/' . $imageName, base64_decode($imageData)); $data['image'] = 'visits/' . $imageName; } // 6️⃣ Création de la visite $visit = Visit::create($data); return response()->json([ 'status' => 'success', 'message' => 'Visite enregistrée avec succès', 'data' => $visit, ], 201); } // ✅ GET d'une visite avec nouvelles colonnes public function show($id) { $visit = Visit::with(['user', 'listing', 'cycle'])->find($id); if (!$visit) { return response()->json([ 'status' => 'error', 'message' => 'Visite non trouvée' ], 404); } return response()->json([ 'status' => 'success', 'data' => $visit ]); } public function update(Request $request, $id) { $visit = Visit::find($id); if (!$visit) { return response()->json([ 'status' => 'error', 'message' => 'Visite non trouvée' ], 404); } $validator = Validator::make($request->all(), [ // mêmes règles que pour store 'listing_id' => 'integer|exists:listings,id', 'visite_type' => 'string|in:Face à face,Double,Accompagné', 'user_latitude' => 'nullable|numeric', 'user_longitude' => 'nullable|numeric', 'distance_meters' => 'nullable|numeric', 'admin_pending' => 'boolean', 'interlocuteur' => 'nullable|string|max:255', 'role' => 'nullable|string|max:255', 'rupture_stock' => 'boolean', 'marche_direct' => 'boolean', 'objection' => 'boolean', 'important' => 'boolean', 'elapsed_time' => 'integer|min:0', 'proposed_meds' => 'nullable|array', 'requested_meds' => 'nullable|array', 'results' => 'nullable|array', 'notes' => 'nullable|string', 'status' => 'nullable|in:valide,invalide,double,non_realisee,distante', 'user_accompagnant_id' => 'nullable|integer|exists:users,id', 'poslat' => 'nullable|numeric', 'poslong' => 'nullable|numeric', 'has_ticket' => 'boolean', 'is_far' => 'boolean', 'far_reason' => 'nullable|string|max:255', 'far_explanation' => 'nullable|string', // 'image' => 'nullable|string', ]); if ($validator->fails()) { return response()->json([ 'status' => 'error', 'message' => 'Validation failed', 'errors' => $validator->errors(), ], 422); } $data = $validator->validated(); // ✅ gérer l'image si base64 if (!empty($data['image']) && Str::startsWith($data['image'], 'data:image')) { $imageData = explode(',', $data['image'])[1]; $imageName = Str::uuid() . '.png'; Storage::disk('public')->put('visits/' . $imageName, base64_decode($imageData)); $data['image'] = 'visits/' . $imageName; } $visit->update($data); return response()->json([ 'status' => 'success', 'message' => 'Visite mise à jour', 'data' => $visit ]); } public function destroy($id) { $visit = Visit::find($id); if (!$visit) { return response()->json([ 'status' => 'error', 'message' => 'Visite non trouvée' ], 404); } $visit->delete(); return response()->json([ 'status' => 'success', 'message' => 'Visite supprimée' ]); } public function today(Request $request) { try { $user = $request->user(); if (!$user) { return response()->json([ 'status' => 'error', 'message' => 'Utilisateur non authentifié' ], 401); } // ⚡ Préciser la table pour éviter l'ambiguïté $cityIds = $user->cities()->pluck('cities.id'); if ($cityIds->isEmpty()) { return response()->json([ 'status' => 'error', 'message' => 'Aucune ville associée à cet utilisateur' ], 422); } $today = Carbon::today(); $stats = Visit::join('listings', 'visits.listing_id', '=', 'listings.id') ->whereDate('visits.date_visit', $today) ->whereIn('listings.city_id', $cityIds) ->selectRaw('listings.type, COUNT(*) as total') ->groupBy('listings.type') ->get(); return response()->json([ 'status' => 'success', 'data' => $stats ]); } catch (\Exception $e) { return response()->json([ 'status' => 'error', 'message' => 'Une erreur est survenue : ' . $e->getMessage() ], 500); } } } ----- ./API/UserController.php ----- id(); $users = User::select('id', 'name', 'email') ->where('id', '!=', $connectedUserId) ->get(); return response()->json([ 'status' => 'success', 'users' => $users ]); } /** * Show the form for creating a new resource. */ public function create() { // } /** * Store a newly created resource in storage. */ public function store(Request $request) { // } /** * Display the specified resource. */ public function show(string $id) { // } /** * Show the form for editing the specified resource. */ public function edit(string $id) { // } public function updateProfile(Request $request) { $user = auth()->user(); $validated = $request->validate([ 'name' => ['sometimes', 'string', 'max:255'], 'email' => [ 'sometimes', 'email', Rule::unique('users')->ignore($user->id) ], 'phone' => ['nullable', 'string', 'max:20'], 'mobile' => ['nullable', 'string', 'max:20'], 'address' => ['nullable', 'string'], 'birth_date' => ['nullable', 'date'], 'national_id' => ['nullable', 'string', 'max:50'], 'car_registration' => ['nullable', 'string', 'max:20'], 'car_model' => ['nullable', 'string', 'max:50'], 'password' => ['nullable', 'confirmed', 'min:8'], // inclure password_confirmation ]); if (!empty($validated['password'])) { $validated['password'] = Hash::make($validated['password']); } else { unset($validated['password']); } $user->update($validated); if (array_key_exists('password', $validated)) { $user->update([ 'password_changed_at' => now(), 'must_change_password' => false ]); } return response()->json([ 'status' => 'success', 'message' => 'Profil mis à jour avec succès.', 'user' => $user, ]); } public function update(Request $request, string $id) { // } /** * Remove the specified resource from storage. */ public function destroy(string $id) { // } } ----- ./API/ExpenseDashboardController.php ----- user(); /* ================= CYCLE ACTUEL ================= */ $cycle = Cycle::whereDate('start_date', '<=', now()) ->whereDate('end_date', '>=', now()) ->first(); $cycleId = optional($cycle)->id; /* ================= PRIME PAR VISITE ================= */ $primePerVisit = SalarySetting::first()?->prime_per_visit ?? 0; /* ================= VISITES ================= */ $visitsCount = Visit::where('user_id', $user->id) ->where('cycle_id', $cycleId) ->where('status', 'valide') ->count(); $visitGain = $visitsCount * $primePerVisit; /* ================= DÉPLACEMENTS ================= */ $deplacementGain = Expense::where('user_id', $user->id) ->where('category', 'transport') ->where('status', 'approuvee') ->sum('amount'); /* ================= HORS TERRAIN ================= */ $horsTerrainGain = Expense::where('user_id', $user->id) ->whereIn('category', ['repas', 'hotel']) ->where('status', 'approuvee') ->sum('amount'); /* ================= AUTRES ================= */ $autresGain = Expense::where('user_id', $user->id) ->where('category', 'autre') ->where('status', 'approuvee') ->sum('amount'); /* ================= TOTAL ================= */ $total = $visitGain + $deplacementGain + $horsTerrainGain + $autresGain; /* ================= STATS ================= */ $stats = [ [ 'label' => 'Visite', 'gain' => round($visitGain), 'percent' => $total > 0 ? round(($visitGain / $total) * 100) : 0, ], [ 'label' => 'Déplacement', 'gain' => round($deplacementGain), 'percent' => $total > 0 ? round(($deplacementGain / $total) * 100) : 0, ], [ 'label' => 'Hors terrain', 'gain' => round($horsTerrainGain), 'percent' => $total > 0 ? round(($horsTerrainGain / $total) * 100) : 0, ], [ 'label' => 'Autres', 'gain' => round($autresGain), 'percent' => $total > 0 ? round(($autresGain / $total) * 100) : 0, ], ]; /* ================= OBJECTIFS ================= */ $objectifs = Objective::where('user_id', $user->id) ->where('cycle_id', $cycleId) ->get() ->map(function ($obj) use ($primePerVisit) { $done = $obj->target_visits > 0 ? round(($obj->achieved / $obj->target_visits) * 100) : 0; return [ 'type' => ucfirst($obj->category), 'objectif' => $obj->target_visits, 'realise' => $obj->achieved, 'done' => $done, 'gain' => round($obj->achieved * $primePerVisit), ]; }); /* ================= RESPONSE ================= */ return response()->json([ 'total' => round($total), 'stats' => $stats, 'objectifs' => $objectifs->values(), ]); } } ----- ./API/ObjectiveController.php ----- validate([ 'listing_id' => 'required|exists:listings,id', 'start_lat' => 'required|numeric', 'start_lng' => 'required|numeric', ]); // éviter plusieurs visites en cours $existing = VisitInProgress::where('user_id', auth()->id()) ->where('status', 'in_progress') ->first(); if ($existing) { return response()->json([ 'status' => 'error', 'message' => 'Une visite est déjà en cours' ], 400); } $visit = VisitInProgress::create([ 'user_id' => auth()->id(), 'listing_id' => $data['listing_id'], 'start_time' => now(), 'start_lat' => $data['start_lat'], 'start_lng' => $data['start_lng'], 'status' => 'in_progress', ]); return response()->json([ 'status' => 'success', 'visit' => $visit ]); } // 🧨 FINISH VISIT public function finish(Request $request) { $data = $request->validate([ 'end_lat' => 'required|numeric', 'end_lng' => 'required|numeric', ]); $visit = VisitInProgress::where('user_id', auth()->id()) ->where('status', 'in_progress') ->latest() ->first(); if (!$visit) { return response()->json([ 'status' => 'error', 'message' => 'Aucune visite en cours' ], 404); } $visit->update([ 'end_time' => now(), 'end_lat' => $data['end_lat'], 'end_lng' => $data['end_lng'], 'status' => 'finished', ]); return response()->json([ 'status' => 'success', 'visit' => $visit ]); } } ----- ./API/WeeklyProgramController.php ----- json(Appointment::where('user_id', $request->user()->id)->get()); } public function store(Request $request) { $validated = $request->validate([ 'listing_id' => 'required|integer', 'date_appointment' => 'required|date' ]); $appt = Appointment::create([ 'user_id' => $request->user()->id, ...$validated ]); return response()->json(['message' => 'Rendez-vous ajouté', 'data' => $appt]); } public function destroy($id) { Appointment::findOrFail($id)->delete(); return response()->json(['message' => 'Rendez-vous supprimé']); } } ----- ./API/CycleController.php ----- json(Cycle::all()); } } class ObjectiveController extends Controller { public function index() { return response()->json(Objective::with('cycle')->get()); } } class WeeklyProgramController extends Controller { public function userProgram() { return response()->json(WeeklyProgram::with('cycle','city')->get()); } } class DashboardController extends Controller { public function index() { return response()->json([ 'total_visits' => 38, 'goal' => 50, 'medications_presented' => 13, 'earnings' => 2500 ]); } } ----- ./API/LocationController.php ----- validate([ 'latitude' => 'required|numeric', 'longitude' => 'required|numeric', ]); $location = Location::create([ 'user_id' => $request->user()->id, 'latitude' => $request->latitude, 'longitude' => $request->longitude, 'recorded_at' => now(), ]); \Log::info("User {$request->user()->id} location saved: {$request->latitude}, {$request->longitude}"); return response()->json([ 'status' => 'success', 'location' => $location ]); } public function bulk(Request $request) { $locations = $request->input('locations', []); foreach ($locations as $loc) { Location::create([ 'user_id' => $request->user()->id, 'latitude' => $loc['latitude'], 'longitude' => $loc['longitude'], 'recorded_at' => $loc['recorded_at'] ?? now(), ]); } return response()->json(['status' => 'success', 'count' => count($locations)]); } } ----- ./API/ActionRequestController.php ----- where('user_id', auth()->id()) ->when($request->search, function ($q) use ($request) { $q->where('produit', 'like', "%{$request->search}%") ->orWhere('type', 'like', "%{$request->search}%"); }) ->orderByDesc('created_at') ->get(); return response()->json([ 'data' => $actions ]); } } ----- ./output.txt ----- ----- ./WorkSessionController.php ----- validate([ 'latitude' => 'required|numeric', 'longitude' => 'required|numeric', 'recorded_at' => 'nullable|date' ]); Location::create([ 'user_id' => auth()->id(), 'latitude' => $request->latitude, 'longitude' => $request->longitude, 'recorded_at' => $request->recorded_at ?? now(), ]); return response()->json(['success' => true]); } public function bulk(Request $request) { $locations = $request->locations; foreach ($locations as $loc) { Location::create([ 'user_id' => auth()->id(), 'latitude' => $loc['latitude'], 'longitude' => $loc['longitude'], 'recorded_at' => $loc['recorded_at'], ]); } return response()->json(['success' => true]); } } ----- ./Controller.php -----