473 lines
18 KiB
PHP
473 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']);
|
|
|
|
$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', '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 = 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', '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.');
|
|
}
|
|
}
|