first commit
This commit is contained in:
667
app/Http/Controllers/GeoPlanController.php
Normal file
667
app/Http/Controllers/GeoPlanController.php
Normal file
@ -0,0 +1,667 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\SalesPlan;
|
||||
use App\Models\PlanTarget;
|
||||
use App\Helpers\GeoHelper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class GeoPlanController extends Controller
|
||||
{
|
||||
/**
|
||||
* GET /api/sales-plans
|
||||
* Get all plans with optional filters
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = SalesPlan::with(['user:id,employee_id,name,color', 'targets']);
|
||||
|
||||
// Filter by date
|
||||
if ($request->has('date')) {
|
||||
$date = Carbon::parse($request->date, config('app.timezone'));
|
||||
$query->whereBetween('date', [
|
||||
$date->copy()->startOfDay(),
|
||||
$date->copy()->endOfDay()
|
||||
]);
|
||||
}
|
||||
|
||||
// Filter by salesId
|
||||
if ($request->has('salesId')) {
|
||||
$user = User::where('employee_id', $request->salesId)->first(['id']);
|
||||
if ($user) {
|
||||
$query->where('user_id', $user->id);
|
||||
} else {
|
||||
// User not found, return empty result
|
||||
$query->where('user_id', 'non_existent_id');
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
if ($request->has('status')) {
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
$plans = $query->orderBy('date', 'desc')->get();
|
||||
|
||||
$formattedPlans = $plans->map(function ($plan) {
|
||||
return [
|
||||
'id' => (string) $plan->id,
|
||||
'salesId' => $plan->user->employee_id,
|
||||
'salesName' => $plan->user->name,
|
||||
'salesColor' => $plan->user->color,
|
||||
'date' => $plan->date->format('Y-m-d'),
|
||||
'status' => $plan->status,
|
||||
'createdAt' => $plan->created_at->toISOString(),
|
||||
'targets' => $plan->targets->map(fn($t) => [
|
||||
'id' => (string) $t->id,
|
||||
'lat' => (float) $t->latitude,
|
||||
'lng' => (float) $t->longitude,
|
||||
'name' => $t->name,
|
||||
'address' => $t->address ?? '',
|
||||
'order' => $t->order,
|
||||
'isCompleted' => $t->is_completed ?? false,
|
||||
'completedAt' => $t->completed_at ? $t->completed_at->toISOString() : null,
|
||||
]),
|
||||
'optimizedRoute' => $plan->optimized_route,
|
||||
'totalDistance' => $plan->total_distance_km,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'plans' => $formattedPlans,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sales-plans/dates
|
||||
* Get available dates
|
||||
*/
|
||||
public function dates(): JsonResponse
|
||||
{
|
||||
// MongoDB Aggregation to get unique dates
|
||||
$rawDates = SalesPlan::raw(function ($collection) {
|
||||
return $collection->aggregate([
|
||||
[
|
||||
'$project' => [
|
||||
'dateOnly' => ['$dateToString' => ['format' => '%Y-%m-%d', 'date' => '$date']]
|
||||
]
|
||||
],
|
||||
[
|
||||
'$group' => [
|
||||
'_id' => '$dateOnly'
|
||||
]
|
||||
],
|
||||
[
|
||||
'$sort' => ['_id' => 1] // Ascending for Planner usually, but let's check original. Original was ASC.
|
||||
]
|
||||
]);
|
||||
});
|
||||
|
||||
$dates = collect($rawDates)->map(function ($item) {
|
||||
return $item['_id'];
|
||||
})->values();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'dates' => $dates,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sales-plans/sales
|
||||
* Get sales list
|
||||
*/
|
||||
public function sales(): JsonResponse
|
||||
{
|
||||
$users = User::where('is_active', true)
|
||||
->where('role', 'sales')
|
||||
->get(['id', 'employee_id', 'name', 'color']);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'salesList' => $users->map(fn($u) => [
|
||||
'id' => $u->employee_id,
|
||||
'name' => $u->name,
|
||||
'color' => $u->color,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sales-plans/{id}
|
||||
* Get single plan
|
||||
*/
|
||||
public function show(string $id): JsonResponse
|
||||
{
|
||||
$plan = SalesPlan::with(['user:id,employee_id,name,color', 'targets'])->find($id);
|
||||
|
||||
if (!$plan) {
|
||||
return response()->json(['success' => false, 'error' => 'Plan not found'], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'plan' => [
|
||||
'id' => (string) $plan->id,
|
||||
'salesId' => $plan->user->employee_id,
|
||||
'salesName' => $plan->user->name,
|
||||
'salesColor' => $plan->user->color,
|
||||
'date' => $plan->date->format('Y-m-d'),
|
||||
'status' => $plan->status,
|
||||
'targets' => $plan->targets->map(fn($t) => [
|
||||
'id' => (string) $t->id,
|
||||
'lat' => (float) $t->latitude,
|
||||
'lng' => (float) $t->longitude,
|
||||
'name' => $t->name,
|
||||
'address' => $t->address ?? '',
|
||||
'order' => $t->order,
|
||||
'isCompleted' => $t->is_completed ?? false,
|
||||
'completedAt' => $t->completed_at ? $t->completed_at->toISOString() : null,
|
||||
]),
|
||||
'optimizedRoute' => $plan->optimized_route,
|
||||
'totalDistance' => $plan->total_distance_km,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/sales-plans
|
||||
* Create new plan
|
||||
*/
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'salesId' => 'required|string',
|
||||
'date' => 'required|date',
|
||||
'targets' => 'array',
|
||||
]);
|
||||
|
||||
$user = User::where('employee_id', $request->salesId)->first();
|
||||
if (!$user) {
|
||||
return response()->json(['success' => false, 'error' => 'Invalid salesId'], 400);
|
||||
}
|
||||
|
||||
$plan = SalesPlan::create([
|
||||
'user_id' => $user->id,
|
||||
'date' => $request->date,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
// Create targets
|
||||
$targets = collect($request->targets ?? [])->map(function ($t, $idx) use ($plan) {
|
||||
return PlanTarget::create([
|
||||
'sales_plan_id' => $plan->id,
|
||||
'order' => $idx + 1,
|
||||
'latitude' => $t['lat'],
|
||||
'longitude' => $t['lng'],
|
||||
'name' => $t['name'],
|
||||
'address' => $t['address'] ?? '',
|
||||
]);
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'plan' => [
|
||||
'id' => (string) $plan->id,
|
||||
'salesId' => $user->employee_id,
|
||||
'salesName' => $user->name,
|
||||
'salesColor' => $user->color,
|
||||
'date' => $plan->date->format('Y-m-d'),
|
||||
'status' => $plan->status,
|
||||
'targets' => $targets->map(fn($t) => [
|
||||
'id' => (string) $t->id,
|
||||
'lat' => (float) $t->latitude,
|
||||
'lng' => (float) $t->longitude,
|
||||
'name' => $t->name,
|
||||
'address' => $t->address,
|
||||
'order' => $t->order,
|
||||
'isCompleted' => $t->is_completed ?? false,
|
||||
'completedAt' => $t->completed_at ? $t->completed_at->toISOString() : null,
|
||||
]),
|
||||
'optimizedRoute' => null,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/sales-plans/{id}
|
||||
* Update plan
|
||||
*/
|
||||
public function update(Request $request, string $id): JsonResponse
|
||||
{
|
||||
$plan = SalesPlan::with('user')->find($id);
|
||||
if (!$plan) {
|
||||
return response()->json(['success' => false, 'error' => 'Plan not found'], 404);
|
||||
}
|
||||
|
||||
if ($request->has('targets')) {
|
||||
// Delete existing targets
|
||||
$plan->targets()->delete();
|
||||
|
||||
// Create new targets
|
||||
foreach ($request->targets as $idx => $t) {
|
||||
PlanTarget::create([
|
||||
'sales_plan_id' => $plan->id,
|
||||
'order' => $idx + 1,
|
||||
'latitude' => $t['lat'],
|
||||
'longitude' => $t['lng'],
|
||||
'name' => $t['name'],
|
||||
'address' => $t['address'] ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
$plan->status = 'pending';
|
||||
$plan->optimized_route = null;
|
||||
}
|
||||
|
||||
if ($request->has('date')) {
|
||||
$plan->date = $request->date;
|
||||
}
|
||||
|
||||
if ($request->has('salesId')) {
|
||||
$user = User::where('employee_id', $request->salesId)->first();
|
||||
if ($user) {
|
||||
$plan->user_id = $user->id;
|
||||
}
|
||||
}
|
||||
|
||||
$plan->save();
|
||||
$plan->load(['user', 'targets']);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'plan' => $this->formatPlan($plan),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/sales-plans/{id}
|
||||
* Delete plan
|
||||
*/
|
||||
public function destroy(string $id): JsonResponse
|
||||
{
|
||||
$plan = SalesPlan::find($id);
|
||||
if (!$plan) {
|
||||
return response()->json(['success' => false, 'error' => 'Plan not found'], 404);
|
||||
}
|
||||
|
||||
$plan->targets()->delete();
|
||||
$plan->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Plan deleted',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/sales-plans/{id}/optimize
|
||||
* Optimize route using Nearest Neighbor algorithm
|
||||
*/
|
||||
public function optimize(string $id): JsonResponse
|
||||
{
|
||||
$plan = SalesPlan::with(['user', 'targets'])->find($id);
|
||||
if (!$plan) {
|
||||
return response()->json(['success' => false, 'error' => 'Plan not found'], 404);
|
||||
}
|
||||
|
||||
$targets = $plan->targets->toArray();
|
||||
if (count($targets) < 2) {
|
||||
return response()->json(['success' => false, 'error' => 'Need at least 2 targets to optimize'], 400);
|
||||
}
|
||||
|
||||
// Store original order
|
||||
$originalOrder = array_keys($targets);
|
||||
|
||||
// Run optimization
|
||||
$result = GeoHelper::optimizeRouteNearestNeighbor($targets);
|
||||
$optimizedOrder = $result['order'];
|
||||
$totalDistance = $result['totalDistance'];
|
||||
|
||||
// Reorder targets
|
||||
foreach ($optimizedOrder as $newOrder => $originalIdx) {
|
||||
PlanTarget::where('id', $targets[$originalIdx]['id'])
|
||||
->update(['order' => $newOrder + 1]);
|
||||
}
|
||||
|
||||
// Update plan
|
||||
$plan->status = 'optimized';
|
||||
$plan->optimized_at = now();
|
||||
$plan->optimized_route = $optimizedOrder;
|
||||
$plan->total_distance_km = $totalDistance;
|
||||
$plan->save();
|
||||
|
||||
$plan->load('targets');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'plan' => $this->formatPlan($plan),
|
||||
'optimization' => [
|
||||
'originalOrder' => $originalOrder,
|
||||
'optimizedOrder' => $optimizedOrder,
|
||||
'totalDistanceKm' => $totalDistance,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/sales-plans/{id}/add-target
|
||||
* Add target to existing plan
|
||||
*/
|
||||
public function addTarget(Request $request, string $id): JsonResponse
|
||||
{
|
||||
$plan = SalesPlan::with('user')->find($id);
|
||||
if (!$plan) {
|
||||
return response()->json(['success' => false, 'error' => 'Plan not found'], 404);
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'lat' => 'required|numeric',
|
||||
'lng' => 'required|numeric',
|
||||
'name' => 'required|string',
|
||||
]);
|
||||
|
||||
$maxOrder = $plan->targets()->max('order') ?? 0;
|
||||
|
||||
$target = PlanTarget::create([
|
||||
'sales_plan_id' => $plan->id,
|
||||
'order' => $maxOrder + 1,
|
||||
'latitude' => $request->lat,
|
||||
'longitude' => $request->lng,
|
||||
'name' => $request->name,
|
||||
'address' => $request->address ?? '',
|
||||
'source' => 'mobile_manual', // Differentiate from admin plan
|
||||
]);
|
||||
|
||||
$plan->status = 'pending';
|
||||
$plan->optimized_route = null;
|
||||
$plan->save();
|
||||
$plan->load('targets');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'plan' => $this->formatPlan($plan),
|
||||
'newTarget' => [
|
||||
'id' => (string) $target->id,
|
||||
'lat' => (float) $target->latitude,
|
||||
'lng' => (float) $target->longitude,
|
||||
'name' => $target->name,
|
||||
'address' => $target->address,
|
||||
'order' => $target->order,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/sales-plans/{id}/target/{targetId}
|
||||
* Update existing target
|
||||
*/
|
||||
public function updateTarget(Request $request, string $id, string $targetId): JsonResponse
|
||||
{
|
||||
$plan = SalesPlan::find($id);
|
||||
if (!$plan) {
|
||||
return response()->json(['success' => false, 'error' => 'Plan not found'], 404);
|
||||
}
|
||||
|
||||
$target = PlanTarget::where('id', $targetId)
|
||||
->where('sales_plan_id', $plan->id)
|
||||
->first();
|
||||
|
||||
if (!$target) {
|
||||
return response()->json(['success' => false, 'error' => 'Target not found'], 404);
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'lat' => 'nullable|numeric',
|
||||
'lng' => 'nullable|numeric',
|
||||
'name' => 'nullable|string',
|
||||
]);
|
||||
|
||||
if ($request->has('name'))
|
||||
$target->name = $request->name;
|
||||
if ($request->has('address'))
|
||||
$target->address = $request->address;
|
||||
if ($request->has('lat'))
|
||||
$target->latitude = $request->lat;
|
||||
if ($request->has('lng'))
|
||||
$target->longitude = $request->lng;
|
||||
|
||||
$target->save();
|
||||
|
||||
// Mark plan as pending optimization
|
||||
$plan->status = 'pending';
|
||||
$plan->optimized_route = null;
|
||||
$plan->save();
|
||||
$plan->load(['user', 'targets']); // Reload user relation too just in case formatPlan needs it
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'plan' => $this->formatPlan($plan),
|
||||
'target' => [
|
||||
'id' => (string) $target->id,
|
||||
'lat' => (float) $target->latitude,
|
||||
'lng' => (float) $target->longitude,
|
||||
'name' => $target->name,
|
||||
'address' => $target->address,
|
||||
'order' => $target->order,
|
||||
'isCompleted' => $target->is_completed ?? false,
|
||||
'completedAt' => $target->completed_at ? $target->completed_at->toISOString() : null,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/sales-plans/{id}/target/{targetId}
|
||||
* Remove target from plan
|
||||
*/
|
||||
public function removeTarget(string $id, string $targetId): JsonResponse
|
||||
{
|
||||
$plan = SalesPlan::with('user')->find($id);
|
||||
if (!$plan) {
|
||||
return response()->json(['success' => false, 'error' => 'Plan not found'], 404);
|
||||
}
|
||||
|
||||
$target = PlanTarget::where('id', $targetId)
|
||||
->where('sales_plan_id', $plan->id)
|
||||
->first();
|
||||
|
||||
if (!$target) {
|
||||
return response()->json(['success' => false, 'error' => 'Target not found'], 404);
|
||||
}
|
||||
|
||||
$target->delete();
|
||||
|
||||
// Reorder remaining targets
|
||||
$plan->targets()->orderBy('order')->get()->each(function ($t, $idx) {
|
||||
$t->update(['order' => $idx + 1]);
|
||||
});
|
||||
|
||||
$plan->status = 'pending';
|
||||
$plan->optimized_route = null;
|
||||
$plan->save();
|
||||
$plan->load('targets');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'plan' => $this->formatPlan($plan),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/mobile/schedules/{id}/target/{targetId}/checkin
|
||||
* Check-in to target (mark as completed)
|
||||
*/
|
||||
public function checkinTarget(string $id, string $targetId): JsonResponse
|
||||
{
|
||||
$plan = SalesPlan::with('user')->find($id);
|
||||
if (!$plan) {
|
||||
return response()->json(['success' => false, 'error' => 'Plan not found'], 404);
|
||||
}
|
||||
|
||||
$target = PlanTarget::where('id', $targetId)
|
||||
->where('sales_plan_id', $plan->id)
|
||||
->first();
|
||||
|
||||
if (!$target) {
|
||||
return response()->json(['success' => false, 'error' => 'Target not found'], 404);
|
||||
}
|
||||
|
||||
$target->is_completed = true;
|
||||
$target->completed_at = now();
|
||||
$target->save();
|
||||
|
||||
// Refresh plan
|
||||
$plan->load('targets');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'plan' => $this->formatPlan($plan),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/sales-plans/assign-target
|
||||
* Auto assign target to user's plan for today (find or create)
|
||||
*/
|
||||
public function autoAssignTarget(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'staffId' => 'required|string',
|
||||
'placeData' => 'required|array',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$user = User::where('employee_id', $request->staffId)->first();
|
||||
// Fallback: check by mongo ID if not found by employee_id, or just assume input is right ID type
|
||||
if (!$user) {
|
||||
$user = User::find($request->staffId);
|
||||
}
|
||||
|
||||
if (!$user) {
|
||||
return response()->json(['success' => false, 'error' => 'Staff/User not found'], 404);
|
||||
}
|
||||
|
||||
// Find or create plan for today in user's timezone (or app default)
|
||||
$today = now()->startOfDay();
|
||||
|
||||
$plan = SalesPlan::where('user_id', $user->id)
|
||||
->whereDate('date', $today)
|
||||
->first();
|
||||
|
||||
if (!$plan) {
|
||||
$plan = SalesPlan::create([
|
||||
'user_id' => $user->id,
|
||||
'date' => $today,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
}
|
||||
|
||||
$place = $request->placeData;
|
||||
|
||||
// Calculate order
|
||||
$maxOrder = $plan->targets()->max('order') ?? 0;
|
||||
|
||||
$target = PlanTarget::create([
|
||||
'sales_plan_id' => $plan->id,
|
||||
'order' => $maxOrder + 1,
|
||||
'latitude' => $place['lat'],
|
||||
'longitude' => $place['lng'],
|
||||
'name' => $place['name'],
|
||||
'address' => $place['address'] ?? '',
|
||||
'notes' => $request->notes,
|
||||
'source' => 'web_assign',
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Target assigned successfully',
|
||||
'planId' => $plan->id
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format plan for response
|
||||
*/
|
||||
private function formatPlan(SalesPlan $plan): array
|
||||
{
|
||||
return [
|
||||
'id' => (string) $plan->id,
|
||||
'salesId' => $plan->user->employee_id,
|
||||
'salesName' => $plan->user->name,
|
||||
'salesColor' => $plan->user->color,
|
||||
'date' => $plan->date->format('Y-m-d'),
|
||||
'status' => $plan->status,
|
||||
'targets' => $plan->targets->map(fn($t) => [
|
||||
'id' => (string) $t->id,
|
||||
'lat' => (float) $t->latitude,
|
||||
'lng' => (float) $t->longitude,
|
||||
'name' => $t->name,
|
||||
'address' => $t->address ?? '',
|
||||
'order' => $t->order,
|
||||
'isCompleted' => $t->is_completed ?? false,
|
||||
'completedAt' => $t->completed_at ? $t->completed_at->toISOString() : null,
|
||||
]),
|
||||
'optimizedRoute' => $plan->optimized_route,
|
||||
'totalDistance' => $plan->total_distance_km,
|
||||
];
|
||||
}
|
||||
/**
|
||||
* GET /api/mobile/schedules
|
||||
* Get plan for specific user (for mobile app)
|
||||
*/
|
||||
public function getMobileSchedule(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'user_id' => 'required|string', // Employee ID
|
||||
'date' => 'nullable|date',
|
||||
]);
|
||||
|
||||
// Find user
|
||||
$user = User::where('employee_id', $request->user_id)->first();
|
||||
if (!$user) {
|
||||
return response()->json(['success' => false, 'error' => 'User not found'], 404);
|
||||
}
|
||||
|
||||
// Get the date string for comparison (YYYY-MM-DD format)
|
||||
$dateStr = $request->date ?? now()->toDateString();
|
||||
|
||||
// Create date range for the entire day in local timezone (Asia/Jakarta)
|
||||
// This handles plans created with different timezone offsets
|
||||
$startOfDay = \Carbon\Carbon::parse($dateStr, config('app.timezone'))->startOfDay();
|
||||
$endOfDay = \Carbon\Carbon::parse($dateStr, config('app.timezone'))->endOfDay();
|
||||
|
||||
try {
|
||||
// First, try to find an existing plan for this user on this date
|
||||
$plan = SalesPlan::where('user_id', $user->id)
|
||||
->whereBetween('date', [$startOfDay, $endOfDay])
|
||||
->first();
|
||||
|
||||
// If no plan exists, create one
|
||||
if (!$plan) {
|
||||
$plan = SalesPlan::create([
|
||||
'user_id' => $user->id,
|
||||
'date' => $startOfDay, // Use local timezone start of day
|
||||
'status' => 'pending',
|
||||
'total_distance_km' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
$plan->load('targets');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'plan' => $this->formatPlan($plan),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Server Error: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user