validate([ 'verification_status' => ['nullable', 'in:pending,approved,rejected'], 'barangay_id' => ['nullable', 'integer'], 'service_area_id' => ['nullable', 'integer'], 'q' => ['nullable', 'string', 'max:100'], 'per_page' => ['nullable', 'integer', 'min:1', 'max:500'], ]); $perPage = (int) $request->input('per_page', 25); $households = Household::query() ->with(['head', 'barangay.cityMunicipality', 'barangay.serviceAreas', 'assignedDropOffPoint', 'tenant']) ->withCount('members') ->when( $request->filled('verification_status'), fn ($q) => $q->where('verification_status', $request->string('verification_status')), ) ->when( $request->filled('barangay_id'), fn ($q) => $q->where('barangay_id', $request->integer('barangay_id')), ) ->when( $request->filled('service_area_id'), fn ($q) => $q->whereHas('barangay.serviceAreas', fn ($sa) => $sa->where('service_areas.id', $request->integer('service_area_id'))), ) ->when($request->filled('q'), function ($q) use ($request) { $term = '%'.$request->string('q').'%'; $q->where(function ($qq) use ($term) { $qq->where('address_line', 'like', $term) ->orWhereHas('head', function ($h) use ($term) { $h->where('first_name', 'like', $term) ->orWhere('last_name', 'like', $term) ->orWhere('email', 'like', $term); }); }); }) ->orderByDesc('id') ->paginate($perPage); return $this->ok( HouseholdResource::collection($households), null, [ 'page' => $households->currentPage(), 'per_page' => $households->perPage(), 'total' => $households->total(), 'last_page' => $households->lastPage(), ], ); } public function show(Household $household): JsonResponse { $household->load(['head', 'barangay.cityMunicipality.province', 'barangay.serviceAreas', 'members.user']) ->loadCount('members'); return $this->ok(new HouseholdResource($household)); } public function approve(Request $request, Household $household): JsonResponse { if ($household->verification_status === Household::VERIFICATION_APPROVED) { return $this->fail('Household already approved', null, 422); } if (! $household->proof_of_residency_path) { return $this->fail( 'Cannot approve without proof of residency on file', ['proof' => ['missing']], 422, ); } $household->markVerified($request->user()); HouseholdVerified::dispatch($household->fresh(), $request->user()); $household->load(['head', 'barangay.serviceAreas'])->loadCount('members'); return $this->ok(new HouseholdResource($household), 'Household approved'); } public function reject(RejectProfileRequest $request, Household $household): JsonResponse { $reason = $request->validated('reason'); $household->markRejected($request->user(), $reason); if ($household->head) { Notification::send( $household->head, new HouseholdRejected($household, $reason), ); } $household->load(['head', 'barangay.serviceAreas'])->loadCount('members'); return $this->ok(new HouseholdResource($household), 'Household rejected'); } public function store( StoreAdminHouseholdRequest $request, DropOffPointFinder $dopFinder, ): JsonResponse { $data = $request->validated(); $tenant = Tenancy::current(); $tenantId = null; if ($tenant) { $tenantId = $tenant->id; } else { // Fall back to resolving tenant from selected Barangay (useful for super-admin context) $barangay = Barangay::find($data['barangay_id']); if ($barangay) { $tenantId = Tenant::where('city_municipality_id', $barangay->city_municipality_id)->value('id'); } } if (! $tenantId) { // Fall back to the first available Tenant in the database to prevent blocking super-admins $tenantId = Tenant::value('id'); } if (! $tenantId) { return $this->fail('Could not resolve LGU/Tenant context for the selected Barangay.', null, 400); } $headUserId = $data['head_user_id'] ?? null; if ($data['resident_type'] === 'existing') { $existing = Household::where('head_user_id', $headUserId)->exists(); if ($existing) { return $this->fail( 'Selected resident already heads a household.', ['head_user_id' => ['already_has_household']], 422 ); } } $point = new Point((float) $data['lat'], (float) $data['lng'], 4326); $nearestDop = $dopFinder->nearest((float) $data['lat'], (float) $data['lng']); $proofPath = null; if ($request->hasFile('proof')) { $proofPath = $request->file('proof')->store('households/proofs', 'local'); } $household = DB::transaction(function () use ($data, $tenantId, $point, $nearestDop, &$headUserId, $proofPath) { if ($data['resident_type'] === 'new') { $user = User::create([ 'tenant_id' => $tenantId, 'first_name' => $data['first_name'], 'last_name' => $data['last_name'], 'email' => $data['email'], 'phone' => $data['phone'], 'password' => Hash::make($data['password'] ?? Str::random(12)), 'role' => User::ROLE_RESIDENT, 'status' => User::STATUS_PENDING, 'preferred_language' => 'en', ]); $user->assignRole(User::ROLE_RESIDENT); $profileClass = User::profileModelForRole(User::ROLE_RESIDENT); if ($profileClass) { $profileClass::create(['user_id' => $user->id]); } NotificationPreference::create([ 'user_id' => $user->id, 'language' => 'en', ]); $headUserId = $user->id; } $h = Household::create([ 'tenant_id' => $tenantId, 'head_user_id' => $headUserId, 'barangay_id' => $data['barangay_id'], 'address_line' => $data['address_line'], 'coordinates' => $point, 'household_size' => $data['household_size'], 'assigned_drop_off_point_id' => $nearestDop?->id, 'verification_status' => Household::VERIFICATION_PENDING, 'proof_of_residency_path' => $proofPath, ]); HouseholdMember::create([ 'household_id' => $h->id, 'user_id' => $headUserId, 'relationship' => HouseholdMember::RELATIONSHIP_HEAD, 'full_name' => User::find($headUserId)->full_name, ]); return $h; }); $household->load(['head', 'barangay.serviceAreas', 'members.user', 'assignedDropOffPoint'])->loadCount('members'); return $this->created( new HouseholdResource($household), 'Household created successfully.' ); } public function update( UpdateAdminHouseholdRequest $request, Household $household, DropOffPointFinder $dopFinder, ): JsonResponse { $data = $request->validated(); $point = new Point((float) $data['lat'], (float) $data['lng'], 4326); $barangay = Barangay::findOrFail($data['barangay_id']); if ($barangay && $barangay->boundary) { $isContained = DB::selectOne( 'SELECT ST_Contains(boundary, ST_GeomFromText(?, 4326, \'axis-order=long-lat\')) as contained FROM barangays WHERE id = ?', [$point->toWkt(), $barangay->id] ); if (! $isContained || ! $isContained->contained) { return $this->fail( 'The pinned household coordinates must lie completely within the Barangay boundary.', ['barangay_id' => ['The location pin is outside the selected Barangay boundary.']], 422 ); } } $nearestDop = $dopFinder->nearest((float) $data['lat'], (float) $data['lng']); DB::transaction(function () use ($request, $data, $household, $point, $nearestDop) { $household->update([ 'address_line' => $data['address_line'], 'coordinates' => $point, 'barangay_id' => $data['barangay_id'], 'household_size' => $data['household_size'], 'assigned_drop_off_point_id' => $nearestDop?->id, ]); // Allow changing the household head user if (! empty($data['head_user_id'])) { $newHeadId = (int) $data['head_user_id']; $household->update(['head_user_id' => $newHeadId]); // Keep the household_members head record in sync $household->members()->where('relationship', HouseholdMember::RELATIONSHIP_HEAD) ->update(['user_id' => $newHeadId]); } // Update head user details if provided $headUser = $household->fresh()->head; if ($headUser) { $userFields = []; if ($request->filled('first_name')) { $userFields['first_name'] = $data['first_name']; } if ($request->filled('last_name')) { $userFields['last_name'] = $data['last_name']; } if ($request->filled('email')) { $userFields['email'] = $data['email']; } if ($request->filled('phone')) { $userFields['phone'] = $data['phone']; } if ($request->filled('password')) { $userFields['password'] = Hash::make($data['password']); } if (!empty($userFields)) { $headUser->update($userFields); // Keep the household_members head record full name in sync $household->members() ->where('user_id', $headUser->id) ->where('relationship', HouseholdMember::RELATIONSHIP_HEAD) ->update(['full_name' => $headUser->fresh()->full_name]); } } // Replace proof document if uploaded if ($request->hasFile('proof')) { if ($household->proof_of_residency_path) { Storage::disk('local')->delete($household->proof_of_residency_path); } $proofPath = $request->file('proof')->store('households/proofs', 'local'); $household->update(['proof_of_residency_path' => $proofPath]); } }); $household->load(['head', 'barangay.serviceAreas', 'members.user', 'assignedDropOffPoint'])->loadCount('members'); return $this->ok(new HouseholdResource($household), 'Household updated successfully.'); } // Task 3: serve proof-of-residency file to admins public function proof(Household $household): StreamedResponse { abort_if(! $household->proof_of_residency_path, 404, 'No proof on file.'); return Storage::disk('local')->download($household->proof_of_residency_path); } // Task 4: manually override the assigned drop-off point public function overrideDop(Request $request, Household $household): JsonResponse { $request->validate([ 'drop_off_point_id' => ['required', 'integer', 'exists:drop_off_points,id'], ]); $household->update([ 'assigned_drop_off_point_id' => $request->integer('drop_off_point_id'), ]); $household->load(['head', 'barangay.serviceAreas', 'assignedDropOffPoint'])->loadCount('members'); return $this->ok(new HouseholdResource($household), 'Drop-off point updated.'); } public function addMember(StoreAdminHouseholdMemberRequest $request, Household $household): JsonResponse { $data = $request->validated(); if (! empty($data['user_id'])) { $isMemberOfCurrent = HouseholdMember::where('user_id', $data['user_id']) ->where('household_id', $household->id) ->exists(); if ($isMemberOfCurrent) { return $this->fail('The selected resident already belongs to this household.', ['user_id' => ['already_member_here']], 422); } $user = User::findOrFail($data['user_id']); $fullName = $user->full_name; } else { $fullName = $data['full_name']; } if ($data['relationship'] === HouseholdMember::RELATIONSHIP_HEAD) { $hasHead = $household->members()->where('relationship', HouseholdMember::RELATIONSHIP_HEAD)->exists(); if ($hasHead) { return $this->fail('Household already has a designated head.', ['relationship' => ['head_already_exists']], 422); } } $member = DB::transaction(function () use ($data, $household, $fullName) { if (! empty($data['user_id'])) { // Find all existing memberships of this user $existingMemberships = HouseholdMember::where('user_id', $data['user_id'])->get(); foreach ($existingMemberships as $oldMember) { $oldHousehold = $oldMember->household; if ($oldHousehold) { // Delete the old membership $oldMember->delete(); // If they were the head of that old household if ($oldHousehold->head_user_id == $data['user_id']) { $otherMembers = $oldHousehold->members()->where('user_id', '!=', $data['user_id'])->get(); if ($otherMembers->isEmpty()) { // Delete empty household $oldHousehold->delete(); } else { // Reassign head to next available member $nextHead = $otherMembers->first(); $oldHousehold->update([ 'head_user_id' => $nextHead->user_id, ]); $nextHead->update([ 'relationship' => HouseholdMember::RELATIONSHIP_HEAD, ]); } } else { if ($oldHousehold->members()->count() === 0) { $oldHousehold->delete(); } } } } } $m = null; if (! empty($data['user_id'])) { $trashed = HouseholdMember::onlyTrashed() ->where('household_id', $household->id) ->where('user_id', $data['user_id']) ->first(); if ($trashed) { $trashed->restore(); $trashed->update([ 'relationship' => $data['relationship'], 'full_name' => $fullName, 'date_of_birth' => $data['date_of_birth'] ?? null, ]); $m = $trashed; } } if (! $m) { $m = HouseholdMember::create([ 'household_id' => $household->id, 'user_id' => $data['user_id'] ?? null, 'relationship' => $data['relationship'], 'full_name' => $fullName, 'date_of_birth' => $data['date_of_birth'] ?? null, ]); } if ($data['relationship'] === HouseholdMember::RELATIONSHIP_HEAD && ! empty($data['user_id'])) { $household->update(['head_user_id' => $data['user_id']]); } $currentMemberCount = $household->members()->count(); if ($household->household_size < $currentMemberCount) { $household->update(['household_size' => $currentMemberCount]); } return $m; }); $household->load(['head', 'barangay.serviceAreas', 'members.user', 'assignedDropOffPoint'])->loadCount('members'); return $this->created(new HouseholdResource($household), 'Household member added successfully.'); } public function updateMember( UpdateAdminHouseholdMemberRequest $request, Household $household, HouseholdMember $member ): JsonResponse { if ($member->household_id !== $household->id) { return $this->fail('Member does not belong to this household.', null, 404); } $data = $request->validated(); if ($data['relationship'] === HouseholdMember::RELATIONSHIP_HEAD && $member->relationship !== HouseholdMember::RELATIONSHIP_HEAD) { $otherHead = $household->members() ->where('relationship', HouseholdMember::RELATIONSHIP_HEAD) ->where('id', '!=', $member->id) ->first(); if ($otherHead) { return $this->fail('Household already has another designated head.', ['relationship' => ['head_already_exists']], 422); } if ($member->user_id) { $household->update(['head_user_id' => $member->user_id]); } } if ($member->relationship === HouseholdMember::RELATIONSHIP_HEAD && $data['relationship'] !== HouseholdMember::RELATIONSHIP_HEAD) { return $this->fail('Cannot demote the household head. Designate another member as head first.', ['relationship' => ['cannot_demote_head']], 422); } $member->update([ 'relationship' => $data['relationship'], 'full_name' => $data['full_name'], 'date_of_birth' => $data['date_of_birth'] ?? null, ]); $household->load(['head', 'barangay.serviceAreas', 'members.user', 'assignedDropOffPoint'])->loadCount('members'); return $this->ok(new HouseholdResource($household), 'Household member updated successfully.'); } public function removeMember(Request $request, Household $household, HouseholdMember $member): JsonResponse { if ($member->household_id !== $household->id) { return $this->fail('Member does not belong to this household.', null, 404); } if ($member->relationship === HouseholdMember::RELATIONSHIP_HEAD) { return $this->fail('Cannot remove the household head. Assign a new head first.', null, 422); } $member->delete(); $household->load(['head', 'barangay.serviceAreas', 'members.user', 'assignedDropOffPoint'])->loadCount('members'); return $this->ok(new HouseholdResource($household), 'Household member removed successfully.'); } }