where(function ($outerQuery) use ($request) { if ($request->filled('full_name')) { $outerQuery->orWhere('full_name', 'LIKE', '%' . $request->input('full_name') . '%'); } if ($request->filled('christian_name')) { $outerQuery->orWhere('christian_name', 'LIKE', '%' . $request->input('christian_name') . '%'); } if ($request->filled('surname')) { $outerQuery->orWhere('surname', 'LIKE', '%' . $request->input('surname') . '%'); } if ($request->filled('place_of_birth')) { $outerQuery->orWhere('place_of_birth', 'LIKE', '%' . $request->input('place_of_birth') . '%'); } if ($request->filled('occupation')) { $outerQuery->orWhere('occupation', 'LIKE', '%' . $request->input('occupation') . '%'); } if ($request->filled('date_of_birth')) { $outerQuery->orWhereDate('date_of_birth', $request->input('date_of_birth')); } if ($request->filled('arrival_from') || $request->filled('arrival_to')) { $outerQuery->orWhereHas('migration', function ($mq) use ($request) { if ($request->filled('arrival_from') && $request->filled('arrival_to')) { $mq->whereBetween('date_of_arrival_nt', [ $request->input('arrival_from'), $request->input('arrival_to') ]); } elseif ($request->filled('arrival_from')) { $mq->where('date_of_arrival_nt', '>=', $request->input('arrival_from')); } elseif ($request->filled('arrival_to')) { $mq->where('date_of_arrival_nt', '<=', $request->input('arrival_to')); } }); } if ($request->filled('town_or_city')) { $outerQuery->orWhereHas('residence', function ($rq) use ($request) { $rq->where('town_or_city', 'LIKE', '%' . $request->input('town_or_city') . '%'); }); } }); // Sorting $sortBy = $request->input('sort_by', 'created_at'); $sortOrder = $request->input('sort_order', 'desc'); $secondarySortBy = $request->input('secondary_sort_by'); $secondarySortOrder = $request->input('secondary_sort_order', 'asc'); if ($sortBy === 'date_of_arrival_nt') { $query->leftJoin('migration', 'migration.person_id', '=', 'person.person_id'); $joinedMigration = true; $query->select('person.*')->orderBy('migration.date_of_arrival_nt', $sortOrder); } elseif (in_array($sortBy, ['full_name', 'christian_name', 'surname'])) { $query->orderBy("person.$sortBy", $sortOrder); } else { $query->orderBy('person.created_at', 'desc'); } if ($secondarySortBy === 'date_of_arrival_nt') { if (!$joinedMigration) { $query->leftJoin('migration', 'migration.person_id', '=', 'person.person_id'); $query->select('person.*'); } $query->orderBy('migration.date_of_arrival_nt', $secondarySortOrder); } elseif (in_array($secondarySortBy, ['full_name', 'christian_name', 'surname'])) { $query->orderBy("person.$secondarySortBy", $secondarySortOrder); } // Pagination $perPage = $request->input('per_page', 10); $results = $query->paginate($perPage); // Optionally map photo URLs (if you need full URLs or extra processing) $results->getCollection()->transform(function ($person) { $person->photos->transform(function ($photo) { $photo->url = asset('storage/' . $photo->path); // or whatever logic you have return $photo; }); return $person; }); return response()->json([ 'success' => true, 'data' => $results, 'message' => 'Persons retrieved successfully', ]); } catch (Exception $e) { \Log::error('Error retrieving persons: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Failed to retrieve persons', 'error' => $e->getMessage(), ], 500); } } public function show(string $id): JsonResponse { try { $person = Person::with(array_merge($this->relations, ['photos']))->find($id); if ($person) { // Add full URLs to photos $person->photos->transform(function ($photo) { $photo->url = asset('storage/' . $photo->path); // Adjust if needed return $photo; }); return $this->successResponse($person, 'Person retrieved successfully'); } return $this->notFoundResponse('Person not found'); } catch (Exception $e) { return $this->errorResponse('Failed to retrieve person', $e); } } public function store(StorePersonRequest $request): JsonResponse { try { $data = $request->only([ 'surname', 'christian_name', 'date_of_birth', 'place_of_birth', 'date_of_death', 'occupation', 'additional_notes', 'reference', 'id_card_no' ]); $data['full_name'] = trim("{$request->christian_name} {$request->surname}"); $person = Person::create($data); // Handle related data foreach ($this->relations as $relation) { if ($request->has($relation)) { $person->$relation()->create($request->$relation); } } // Handle photo uploads $uploadedPhotos = []; if ($request->hasFile('photos')) { $photoController = new PhotoController(); // Or inject this via DI $uploadedPhotos = $photoController->handlePhotoUpload( $request->file('photos'), $person->person_id, $request->input('captions', []), $request->input('main_photo_index') // Pass the index directly ); } // Load all relations including photos $person->load(array_merge($this->relations, ['photos'])); return $this->successResponse([ 'person' => $person, 'uploaded_photos' => $uploadedPhotos ], 'Person created successfully', 201); } catch (Exception $e) { return $this->errorResponse('Failed to create person', $e); } } public function update(UpdatePersonRequest $request, string $id): JsonResponse { try { $person = Person::findOrFail($id); $data = $request->only([ 'surname', 'christian_name', 'date_of_birth', 'place_of_birth', 'date_of_death', 'occupation', 'additional_notes', 'reference', 'id_card_no' ]); if ($request->hasAny(['christian_name', 'surname'])) { $christian = $request->input('christian_name', $person->christian_name); $surname = $request->input('surname', $person->surname); $data['full_name'] = trim("$christian $surname"); } $person->update($data); foreach ($this->relations as $relation) { if (is_array($request->$relation ?? null)) { $person->$relation ? $person->$relation->update($request->$relation) : $person->$relation()->create($request->$relation); } } // Handle photo uploads during update $uploadedPhotos = []; if ($request->hasFile('photos')) { $uploadedPhotos = $this->handlePhotoUpload($request, $person->person_id); } // Handle profile photo setting if ($request->input('set_as_profile')) { $profilePhotoSet = false; // Try to set profile photo for new uploads first if ($request->has('profile_photo_index')) { $index = (int) $request->input('profile_photo_index'); if (isset($uploadedPhotos[$index])) { // Clear all profile photos first Photo::where('person_id', $person->person_id)->update(['is_profile_photo' => false]); // Set the new uploaded photo as profile $uploadedPhotos[$index]->is_profile_photo = true; $uploadedPhotos[$index]->save(); $profilePhotoSet = true; } } // If no new photo was set as profile, try existing photo if (!$profilePhotoSet && $request->has('profile_photo_id')) { $photoId = $request->input('profile_photo_id'); $photo = Photo::where('id', $photoId)->where('person_id', $person->person_id)->first(); if ($photo) { // Clear all profile photos first Photo::where('person_id', $person->person_id)->update(['is_profile_photo' => false]); // Set the existing photo as profile $photo->is_profile_photo = true; $photo->save(); $profilePhotoSet = true; } } // Log if profile photo couldn't be set (for debugging) if (!$profilePhotoSet) { \Log::warning('Failed to set profile photo', [ 'person_id' => $person->person_id, 'profile_photo_index' => $request->input('profile_photo_index'), 'profile_photo_id' => $request->input('profile_photo_id'), 'uploaded_photos_count' => count($uploadedPhotos) ]); } } // Handle existing photo updates (captions, etc.) if ($request->has('existing_photos')) { $existingPhotos = $request->input('existing_photos'); if (is_array($existingPhotos)) { foreach ($existingPhotos as $photoData) { if (isset($photoData['id'])) { $photo = Photo::where('id', $photoData['id']) ->where('person_id', $person->person_id) ->first(); if ($photo && isset($photoData['caption'])) { $photo->caption = $photoData['caption']; $photo->save(); } } } } } return $this->successResponse([ 'person' => $person->load(array_merge($this->relations, ['photos'])), 'uploaded_photos' => $uploadedPhotos ], 'Person updated successfully'); } catch (Exception $e) { return $this->errorResponse('Failed to update person', $e); } } public function destroy(string $id): JsonResponse { try { $person = Person::with($this->relations)->findOrFail($id); foreach ($this->relations as $relation) { $person->$relation?->delete(); } // Delete associated photos if ($person->photos) { foreach ($person->photos as $photo) { $path = 'public/photos/' . $person->person_id . '/' . $photo->filename; if (Storage::exists($path)) { Storage::delete($path); } $photo->delete(); } } $person->delete(); return $this->successResponse(null, 'Person deleted successfully'); } catch (Exception $e) { return $this->errorResponse('Failed to delete person', $e); } } // ========== PHOTO MANAGEMENT METHODS ========== /** * Get all photos for a specific person */ public function getPhotos($personId): JsonResponse { try { $person = Person::findOrFail($personId); $photos = $person->photos()->get(); return $this->successResponse([ 'photos' => $photos, 'profile_photo' => $person->photos()->where('is_profile_photo', true)->first() ], 'Photos retrieved successfully'); } catch (Exception $e) { return $this->errorResponse('Failed to retrieve photos', $e); } } /** * Upload photos for a person */ public function uploadPhotos(Request $request, $personId): JsonResponse { $validator = Validator::make($request->all(), [ 'photos' => 'required|array', 'photos.*' => 'required|image|max:10240', // Max 10MB per image 'captions' => 'nullable|array', 'captions.*' => 'nullable|string|max:255', 'set_as_profile' => 'nullable|boolean', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'errors' => $validator->errors() ], 422); } try { // Find the person $person = Person::findOrFail($personId); $uploadedPhotos = $this->handlePhotoUpload($request, $personId); return $this->successResponse([ 'uploaded_photos' => $uploadedPhotos, 'total_photos' => $person->photos()->count() ], 'Photos uploaded successfully'); } catch (Exception $e) { return $this->errorResponse('Failed to upload photos', $e); } } /** * Set a photo as the profile photo */ public function setAsProfilePhoto($photoId): JsonResponse { try { $photo = Photo::findOrFail($photoId); // Remove profile photo flag from other photos of the same person Photo::where('person_id', $photo->person_id) ->update(['is_profile_photo' => false]); // Set this photo as profile photo $photo->is_profile_photo = true; $result = $photo->save(); return $this->successResponse($photo, $result ? 'Profile photo set successfully' : 'Failed to set profile photo'); } catch (Exception $e) { return $this->errorResponse('Failed to set profile photo', $e); } } /** * Update photo caption */ public function updatePhotoCaption(Request $request, $photoId): JsonResponse { $validator = Validator::make($request->all(), [ 'caption' => 'required|string|max:255', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'errors' => $validator->errors() ], 422); } try { $photo = Photo::findOrFail($photoId); $photo->caption = $request->input('caption'); $photo->save(); return $this->successResponse($photo, 'Caption updated successfully'); } catch (Exception $e) { return $this->errorResponse('Failed to update caption', $e); } } /** * Delete a photo */ public function deletePhoto($photoId): JsonResponse { try { $photo = Photo::findOrFail($photoId); // Delete the physical file $path = 'public/photos/' . $photo->person_id . '/' . $photo->filename; if (Storage::exists($path)) { Storage::delete($path); } // If this was a profile photo, try to set another one if ($photo->is_profile_photo) { $nextPhoto = Photo::where('person_id', $photo->person_id) ->where('id', '!=', $photo->id) ->first(); if ($nextPhoto) { $nextPhoto->is_profile_photo = true; $nextPhoto->save(); } } // Delete the database record $photo->delete(); return $this->successResponse(null, 'Photo deleted successfully'); } catch (Exception $e) { return $this->errorResponse('Failed to delete photo', $e); } } // ========== HELPER METHODS ========== /** * Handle photo upload for person creation/update */ private function handlePhotoUpload(Request $request, $personId): array { $uploadedPhotos = []; if ($request->hasFile('photos')) { $files = $request->file('photos'); $captions = $request->input('photo_captions', []); $setAsProfile = $request->input('set_as_profile', false); $profilePhotoSet = false; foreach ($files as $index => $file) { // Validate file if (!$file->isValid() || !in_array($file->getMimeType(), ['image/jpeg', 'image/png', 'image/gif', 'image/webp'])) { continue; } // Generate a unique filename $extension = $file->getClientOriginalExtension(); $filename = Str::uuid() . '.' . $extension; // Store the file $path = $file->storeAs('photos/' . $personId, $filename, 'public'); // Create photo record $photo = new Photo([ 'person_id' => $personId, 'filename' => $filename, 'original_filename' => $file->getClientOriginalName(), 'file_path' => Storage::url($path), 'mime_type' => $file->getMimeType(), 'file_size' => $file->getSize() / 1024, // Convert to KB 'caption' => $captions[$index] ?? null, 'is_profile_photo' => false ]); $photo->save(); $uploadedPhotos[] = $photo; // Set as profile photo if requested and it's the first photo if ($setAsProfile && !$profilePhotoSet) { // Remove profile photo flag from other photos Photo::where('person_id', $personId)->update(['is_profile_photo' => false]); $photo->is_profile_photo = true; $photo->save(); $profilePhotoSet = true; } } } return $uploadedPhotos; } // Response helpers protected function successResponse($data, string $message, int $status = 200): JsonResponse { return response()->json(['success' => true, 'data' => $data, 'message' => $message], $status); } protected function notFoundResponse(string $message): JsonResponse { return response()->json(['success' => false, 'message' => $message], 404); } protected function errorResponse(string $message, Exception $e): JsonResponse { return response()->json([ 'success' => false, 'message' => $message, 'error' => $e->getMessage() ], 500); } }