Files
Verde-Web/app/Http/Controllers/Api/V1/Admin/AdminHouseholdController.php

447 lines
18 KiB
PHP

<?php
namespace App\Http\Controllers\Api\V1\Admin;
use App\Events\HouseholdVerified;
use App\Http\Controllers\Api\V1\ApiController;
use App\Http\Requests\Admin\RejectProfileRequest;
use App\Http\Requests\Admin\StoreAdminHouseholdMemberRequest;
use App\Http\Requests\Admin\StoreAdminHouseholdRequest;
use App\Http\Requests\Admin\UpdateAdminHouseholdMemberRequest;
use App\Http\Requests\Admin\UpdateAdminHouseholdRequest;
use App\Http\Resources\HouseholdResource;
use App\Models\Barangay;
use App\Models\DropOffPoint;
use App\Models\Household;
use App\Models\HouseholdMember;
use App\Models\NotificationPreference;
use App\Models\Tenant;
use App\Models\User;
use App\Notifications\HouseholdRejected;
use App\Services\DropOff\DropOffPointFinder;
use App\Tenancy\Tenancy;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use MatanYadaev\EloquentSpatial\Objects\Point;
use Symfony\Component\HttpFoundation\StreamedResponse;
class AdminHouseholdController extends ApiController
{
public function index(Request $request): JsonResponse
{
$request->validate([
'verification_status' => ['nullable', 'in:pending,approved,rejected'],
'barangay_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'])
->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('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', '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'])->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'])->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']);
$household = DB::transaction(function () use ($data, $tenantId, $point, $nearestDop, &$headUserId) {
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,
]);
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', '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']);
$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,
]);
// Task 2: 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]);
}
$household->load(['head', 'barangay', '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', '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 = 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', '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', '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', 'members.user', 'assignedDropOffPoint'])->loadCount('members');
return $this->ok(new HouseholdResource($household), 'Household member removed successfully.');
}
}