diff --git a/app/Http/Controllers/Api/V1/Household/HouseholdController.php b/app/Http/Controllers/Api/V1/Household/HouseholdController.php index 3603684..835ed1c 100644 --- a/app/Http/Controllers/Api/V1/Household/HouseholdController.php +++ b/app/Http/Controllers/Api/V1/Household/HouseholdController.php @@ -28,11 +28,12 @@ class HouseholdController extends ApiController $data = $request->validated(); $user = $request->user(); - $existing = Household::where('head_user_id', $user->id)->first(); - if ($existing) { + $existingHead = Household::where('head_user_id', $user->id)->exists(); + $existingMember = HouseholdMember::where('user_id', $user->id)->exists(); + if ($existingHead || $existingMember) { return $this->fail( - 'You already have a household. Update it instead.', - ['head_user_id' => ['already_has_household']], + 'You are already part of a household.', + ['head_user_id' => ['You are already in a household.']], 409, ); } @@ -173,13 +174,39 @@ class HouseholdController extends ApiController $this->authorizeOwn($request->user(), $household); $data = $request->validated(); - $member = HouseholdMember::create([ - 'household_id' => $household->id, - 'user_id' => $data['user_id'] ?? null, - 'relationship' => $data['relationship'], - 'full_name' => $data['full_name'] ?? null, - 'date_of_birth' => $data['date_of_birth'] ?? null, - ]); + + $userToAdd = User::where('email', $data['email'])->first(); + + // Check if this user is already a member of any household or the head of one + $isMember = HouseholdMember::where('user_id', $userToAdd->id)->exists(); + $isHead = Household::where('head_user_id', $userToAdd->id)->exists(); + + if ($isMember || $isHead) { + return $this->fail('This user is already part of a household.', ['email' => ['Already in a household.']], 422); + } + + $existingSoftDeleted = HouseholdMember::withTrashed() + ->where('household_id', $household->id) + ->where('user_id', $userToAdd->id) + ->first(); + + if ($existingSoftDeleted) { + $existingSoftDeleted->restore(); + $existingSoftDeleted->update([ + 'relationship' => HouseholdMember::RELATIONSHIP_OTHER, + 'full_name' => $userToAdd->full_name, + 'date_of_birth' => $data['date_of_birth'] ?? null, + ]); + $member = $existingSoftDeleted; + } else { + $member = HouseholdMember::create([ + 'household_id' => $household->id, + 'user_id' => $userToAdd->id, + 'relationship' => HouseholdMember::RELATIONSHIP_OTHER, + 'full_name' => $userToAdd->full_name, + 'date_of_birth' => $data['date_of_birth'] ?? null, + ]); + } return $this->created( new HouseholdResource( diff --git a/app/Http/Controllers/Api/V1/Me/UpcomingPickupsController.php b/app/Http/Controllers/Api/V1/Me/UpcomingPickupsController.php index c046905..bec2d0e 100644 --- a/app/Http/Controllers/Api/V1/Me/UpcomingPickupsController.php +++ b/app/Http/Controllers/Api/V1/Me/UpcomingPickupsController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api\V1\Me; use App\Http\Controllers\Api\V1\ApiController; use App\Models\Household; use App\Models\Trip; +use App\Services\DropOff\DropOffPointFinder; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -16,36 +17,70 @@ class UpcomingPickupsController extends ApiController * always included so the resident sees a "truck on the way" * card even mid-day. */ - public function index(Request $request): JsonResponse + public function index(Request $request, DropOffPointFinder $finder): JsonResponse { $household = Household::with('assignedDropOffPoint') ->where('head_user_id', $request->user()->id) ->first(); - if (! $household || ! $household->assigned_drop_off_point_id) { + if (! $household) { return $this->ok([ 'household_assigned' => false, 'pickups' => [], ]); } - $dopId = $household->assigned_drop_off_point_id; + $nearbyDopIds = []; + + if ($household->coordinates) { + $nearbyDops = $finder->nearby( + $household->coordinates->latitude, + $household->coordinates->longitude, + 2.0, // 2km radius + 10 // max 10 nearby drop-off points + ); + $nearbyDopIds = $nearbyDops->pluck('id')->toArray(); + } + + if ($household->assigned_drop_off_point_id) { + $nearbyDopIds[] = $household->assigned_drop_off_point_id; + } + + $nearbyDopIds = array_unique($nearbyDopIds); + + if (empty($nearbyDopIds)) { + return $this->ok([ + 'household_assigned' => (bool) $household->assigned_drop_off_point_id, + 'drop_off_point' => null, + 'pickups' => [], + ]); + } $trips = Trip::query() ->with(['route:id,name,code', 'team:id,name,driver_id', 'team.driver:id,first_name,last_name', 'truck:id,plate_number']) - ->whereHas('route.stops', fn ($q) => $q->where('drop_off_point_id', $dopId)) - ->where(function ($q) { - $q->where('scheduled_date', '>=', now()->toDateString()) - ->orWhereIn('status', [Trip::STATUS_IN_PROGRESS, Trip::STATUS_AT_DUMPSITE]); - }) + ->whereHas('route.stops', fn ($q) => $q->whereIn('drop_off_point_id', $nearbyDopIds)) ->whereNotIn('status', [Trip::STATUS_CANCELLED, Trip::STATUS_COMPLETED]) + ->where(function ($query) { + // Future dates are always upcoming (evaluated in Manila time) + $query->where('scheduled_date', '>', now('Asia/Manila')->toDateString()) + // For today, check the time + ->orWhere(function ($q) { + $q->where('scheduled_date', now('Asia/Manila')->toDateString()) + ->where(function ($sub) { + // Keep if the time hasn't passed yet (evaluated in Manila time) + $sub->where('scheduled_start_time', '>=', now('Asia/Manila')->toTimeString()) + // OR if the driver has already started the trip + ->orWhereIn('status', [Trip::STATUS_IN_PROGRESS, Trip::STATUS_AT_DUMPSITE]); + }); + }); + }) ->orderBy('scheduled_date') ->orderBy('scheduled_start_time') ->limit(10) ->get(); - $pickups = $trips->map(function (Trip $trip) use ($dopId) { - $myStop = $trip->route?->stops()->where('drop_off_point_id', $dopId)->first(); + $pickups = $trips->map(function (Trip $trip) use ($nearbyDopIds) { + $myStop = $trip->route?->stops()->whereIn('drop_off_point_id', $nearbyDopIds)->first(); return [ 'trip_id' => $trip->uuid, @@ -61,11 +96,11 @@ class UpcomingPickupsController extends ApiController }); return $this->ok([ - 'household_assigned' => true, - 'drop_off_point' => [ - 'id' => $household->assignedDropOffPoint?->uuid, - 'name' => $household->assignedDropOffPoint?->name, - ], + 'household_assigned' => (bool) $household->assigned_drop_off_point_id, + 'drop_off_point' => $household->assignedDropOffPoint ? [ + 'id' => $household->assignedDropOffPoint->uuid, + 'name' => $household->assignedDropOffPoint->name, + ] : null, 'pickups' => $pickups, ]); } diff --git a/app/Http/Requests/Household/StoreHouseholdMemberRequest.php b/app/Http/Requests/Household/StoreHouseholdMemberRequest.php index 6c3ea11..1eec975 100644 --- a/app/Http/Requests/Household/StoreHouseholdMemberRequest.php +++ b/app/Http/Requests/Household/StoreHouseholdMemberRequest.php @@ -16,22 +16,21 @@ class StoreHouseholdMemberRequest extends FormRequest public function rules(): array { return [ - 'user_id' => [ - 'nullable', - 'integer', - Rule::exists('users', 'id')->where(function ($query) { + 'email' => [ + 'required', + 'email', + Rule::exists('users', 'email')->where(function ($query) { $query->where('role', \App\Models\User::ROLE_RESIDENT); }), ], - 'full_name' => ['required_without:user_id', 'nullable', 'string', 'max:191'], - 'relationship' => ['required', Rule::in([ - HouseholdMember::RELATIONSHIP_SPOUSE, - HouseholdMember::RELATIONSHIP_CHILD, - HouseholdMember::RELATIONSHIP_PARENT, - HouseholdMember::RELATIONSHIP_SIBLING, - HouseholdMember::RELATIONSHIP_OTHER, - ])], 'date_of_birth' => ['nullable', 'date', 'before:today'], ]; } + + public function messages(): array + { + return [ + 'email.exists' => 'User does not exist', + ]; + } } diff --git a/config/app.php b/config/app.php index f467267..e2352e7 100644 --- a/config/app.php +++ b/config/app.php @@ -65,7 +65,7 @@ return [ | */ - 'timezone' => env('APP_TIMEZONE', 'UTC'), + 'timezone' => env('APP_TIMEZONE', 'Asia/Manila'), /* |-------------------------------------------------------------------------- diff --git a/test.php b/test.php new file mode 100644 index 0000000..dd83034 --- /dev/null +++ b/test.php @@ -0,0 +1,11 @@ +setUserResolver(fn() => App\Models\User::find(6)); + +$ctrl = app()->make(App\Http\Controllers\Api\V1\Me\UpcomingPickupsController::class); +$f = app()->make(App\Services\DropOff\DropOffPointFinder::class); + +$res = $ctrl->index($req, $f); + +echo json_encode($res->getData(), JSON_PRETTY_PRINT);