initial commit
This commit is contained in:
parent
6848722da3
commit
cac37d000b
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Person;
|
use App\Models\Person;
|
||||||
|
use App\Models\Migration;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
@ -40,6 +41,21 @@ class DashboardController extends Controller
|
||||||
})
|
})
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
|
// Find peak migration period (by year)
|
||||||
|
$peakMigrationYear = Migration::select(DB::raw('YEAR(date_of_arrival_nt) as year'), DB::raw('COUNT(*) as count'))
|
||||||
|
->whereNotNull('date_of_arrival_nt')
|
||||||
|
->groupBy(DB::raw('YEAR(date_of_arrival_nt)'))
|
||||||
|
->orderBy('count', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
// Find most common place of birth
|
||||||
|
$mostCommonOrigin = Person::select('place_of_birth', DB::raw('COUNT(*) as count'))
|
||||||
|
->whereNotNull('place_of_birth')
|
||||||
|
->where('place_of_birth', '!=', '')
|
||||||
|
->groupBy('place_of_birth')
|
||||||
|
->orderBy('count', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => [
|
'data' => [
|
||||||
|
|
@ -47,7 +63,15 @@ class DashboardController extends Controller
|
||||||
'new_this_month' => $newThisMonth,
|
'new_this_month' => $newThisMonth,
|
||||||
'recent_additions' => $recentAdditions,
|
'recent_additions' => $recentAdditions,
|
||||||
'pending_reviews' => $pendingReviews,
|
'pending_reviews' => $pendingReviews,
|
||||||
'incomplete_records' => $incompleteRecords
|
'incomplete_records' => $incompleteRecords,
|
||||||
|
'peak_migration_year' => $peakMigrationYear ? [
|
||||||
|
'year' => $peakMigrationYear->year,
|
||||||
|
'count' => $peakMigrationYear->count
|
||||||
|
] : null,
|
||||||
|
'most_common_origin' => $mostCommonOrigin ? [
|
||||||
|
'place' => $mostCommonOrigin->place_of_birth,
|
||||||
|
'count' => $mostCommonOrigin->count
|
||||||
|
] : null
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use ProtoneMedia\LaravelCrossEloquentSearch\Search;
|
|
||||||
use App\Models\Person;
|
|
||||||
use App\Models\Migration;
|
|
||||||
use App\Models\Residence;
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class HistoricalSearchController extends Controller
|
|
||||||
{
|
|
||||||
public function search(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$query = $request->input('query');
|
|
||||||
$page = (int) $request->input('page', 1);
|
|
||||||
$perPage = 10;
|
|
||||||
|
|
||||||
$results = empty($query)
|
|
||||||
? $this->mergeModels([Person::class, Migration::class, Residence::class])
|
|
||||||
: $this->performSearch($query);
|
|
||||||
|
|
||||||
$paginated = $this->paginateCollection($results, $perPage, $page);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'message' => 'Records fetched successfully',
|
|
||||||
'data' => $paginated->items(),
|
|
||||||
'pagination' => [
|
|
||||||
'total' => $paginated->total(),
|
|
||||||
'perPage' => $paginated->perPage(),
|
|
||||||
'currentPage' => $paginated->currentPage(),
|
|
||||||
'totalPages' => $paginated->lastPage(),
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRecord($id): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$person = Person::with([
|
|
||||||
'migration',
|
|
||||||
'naturalization',
|
|
||||||
'residence',
|
|
||||||
'family',
|
|
||||||
'internment'
|
|
||||||
])->findOrFail($id);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'data' => $person,
|
|
||||||
'message' => 'Record retrieved successfully'
|
|
||||||
]);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
Log::error('Error retrieving record', [
|
|
||||||
'id' => $id,
|
|
||||||
'error' => $e->getMessage()
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'Record not found',
|
|
||||||
'error' => $e->getMessage()
|
|
||||||
], 404);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function paginateCollection(Collection $collection, int $perPage, int $page): LengthAwarePaginator
|
|
||||||
{
|
|
||||||
$items = $collection->forPage($page, $perPage)->values();
|
|
||||||
|
|
||||||
return new LengthAwarePaginator(
|
|
||||||
$items,
|
|
||||||
$collection->count(),
|
|
||||||
$perPage,
|
|
||||||
$page,
|
|
||||||
[
|
|
||||||
'path' => request()->url(),
|
|
||||||
'query' => request()->query(),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function mergeModels(array $models): Collection
|
|
||||||
{
|
|
||||||
return collect($models)
|
|
||||||
->flatMap(fn ($model) => $model::all());
|
|
||||||
}
|
|
||||||
|
|
||||||
private function performSearch(string $query): Collection
|
|
||||||
{
|
|
||||||
return Search::add(Person::class, ['christian_name', 'surname', 'place_of_birth'])
|
|
||||||
->add(Migration::class, ['date_of_arrival_nt'])
|
|
||||||
->add(Residence::class, ['town_or_city'])
|
|
||||||
->beginWithWildcard()
|
|
||||||
->search($query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,551 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\StorePersonRequest;
|
||||||
|
use App\Http\Requests\UpdatePersonRequest;
|
||||||
|
use App\Models\Person;
|
||||||
|
use App\Models\Photo;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class MigrantController extends Controller
|
||||||
|
{
|
||||||
|
protected array $relations = ['migration', 'naturalization', 'residence', 'family', 'internment'];
|
||||||
|
|
||||||
|
public function index(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Include photos in relations
|
||||||
|
$relations = [
|
||||||
|
'migration',
|
||||||
|
'naturalization',
|
||||||
|
'residence',
|
||||||
|
'family',
|
||||||
|
'internment',
|
||||||
|
'photos', // 🔥 Include photos
|
||||||
|
];
|
||||||
|
|
||||||
|
$query = Person::with($relations);
|
||||||
|
$joinedMigration = false;
|
||||||
|
|
||||||
|
// Filtering
|
||||||
|
$query->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,175 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Http\Requests\StorePersonRequest;
|
|
||||||
use App\Http\Requests\UpdatePersonRequest;
|
|
||||||
use App\Models\Person;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class PersonController extends Controller
|
|
||||||
{
|
|
||||||
protected array $relations = ['migration', 'naturalization', 'residence', 'family', 'internment'];
|
|
||||||
|
|
||||||
public function index(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$query = Person::with($this->relations);
|
|
||||||
|
|
||||||
if ($search = $request->search) {
|
|
||||||
$query->where(fn($q) =>
|
|
||||||
$q->where('full_name', 'LIKE', "%$search%")
|
|
||||||
->orWhere('surname', 'LIKE', "%$search%")
|
|
||||||
->orWhere('occupation', 'LIKE', "%$search%")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'data' => $query->paginate(10),
|
|
||||||
'message' => 'Persons retrieved successfully'
|
|
||||||
]);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
return $this->errorResponse('Failed to retrieve persons', $e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function show(string $id): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$person = Person::with($this->relations)->find($id);
|
|
||||||
|
|
||||||
return $person
|
|
||||||
? $this->successResponse($person, 'Person retrieved successfully')
|
|
||||||
: $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);
|
|
||||||
|
|
||||||
foreach ($this->relations as $relation) {
|
|
||||||
if ($request->has($relation)) {
|
|
||||||
$person->$relation()->create($request->$relation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->successResponse(
|
|
||||||
$person->load($this->relations),
|
|
||||||
'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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->successResponse(
|
|
||||||
$person->load($this->relations),
|
|
||||||
'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();
|
|
||||||
}
|
|
||||||
|
|
||||||
$person->delete();
|
|
||||||
|
|
||||||
return $this->successResponse(null, 'Person deleted successfully');
|
|
||||||
} catch (Exception $e) {
|
|
||||||
return $this->errorResponse('Failed to delete person', $e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
public function search(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$query = Person::query()->with($this->relations);
|
|
||||||
|
|
||||||
if ($search = $request->input('query')) {
|
|
||||||
$query->where(function ($q) use ($search) {
|
|
||||||
$q->where('full_name', 'LIKE', "%$search%")
|
|
||||||
->orWhere('surname', 'LIKE', "%$search%")
|
|
||||||
->orWhere('christian_name', 'LIKE', "%$search%")
|
|
||||||
->orWhere('occupation', 'LIKE', "%$search%")
|
|
||||||
->orWhere('place_of_birth', 'LIKE', "%$search%")
|
|
||||||
->orWhere('id_card_no', 'LIKE', "%$search%");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->successResponse(
|
|
||||||
$query->paginate(10),
|
|
||||||
'Search results returned successfully'
|
|
||||||
);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
return $this->errorResponse('Failed to perform search', $e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Person;
|
||||||
|
use App\Models\Photo;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class PhotoController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get all photos for a specific person
|
||||||
|
*/
|
||||||
|
public function getPhotos($personId)
|
||||||
|
{
|
||||||
|
$person = Person::findOrFail($personId);
|
||||||
|
$photos = $person->photos()->get();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $photos,
|
||||||
|
'profile_photo' => $person->photos()->where('is_profile_photo', true)->first()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handlePhotoUpload($files, $personId, $captions = [], $mainPhotoIndex = null)
|
||||||
|
{
|
||||||
|
$uploadedPhotos = [];
|
||||||
|
|
||||||
|
foreach ($files as $index => $file) {
|
||||||
|
$extension = $file->getClientOriginalExtension();
|
||||||
|
$filename = Str::uuid() . '.' . $extension;
|
||||||
|
$path = $file->storeAs('photos/' . $personId, $filename, 'public');
|
||||||
|
|
||||||
|
$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, // KB
|
||||||
|
'caption' => $captions[$index] ?? null,
|
||||||
|
'is_profile_photo' => ($mainPhotoIndex !== null && $index == $mainPhotoIndex)
|
||||||
|
]);
|
||||||
|
|
||||||
|
$photo->save();
|
||||||
|
$uploadedPhotos[] = $photo;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $uploadedPhotos;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload photos for a person
|
||||||
|
*/
|
||||||
|
public function upload(Request $request, $personId)
|
||||||
|
{
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the person
|
||||||
|
$person = Person::findOrFail($personId);
|
||||||
|
$uploadedPhotos = [];
|
||||||
|
|
||||||
|
// Process each uploaded photo
|
||||||
|
if ($request->hasFile('photos')) {
|
||||||
|
$files = $request->file('photos');
|
||||||
|
$captions = $request->input('captions', []);
|
||||||
|
$setAsProfile = $request->input('set_as_profile', false);
|
||||||
|
$profilePhotoSet = false;
|
||||||
|
|
||||||
|
foreach ($files as $index => $file) {
|
||||||
|
// 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) {
|
||||||
|
$photo->setAsProfilePhoto();
|
||||||
|
$profilePhotoSet = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Photos uploaded successfully',
|
||||||
|
'data' => $uploadedPhotos
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a photo as the profile photo
|
||||||
|
*/
|
||||||
|
public function setAsProfilePhoto($photoId)
|
||||||
|
{
|
||||||
|
$photo = Photo::findOrFail($photoId);
|
||||||
|
$result = $photo->setAsProfilePhoto();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => $result,
|
||||||
|
'message' => $result ? 'Profile photo set successfully' : 'Failed to set profile photo',
|
||||||
|
'data' => $photo
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update photo caption
|
||||||
|
*/
|
||||||
|
public function updateCaption(Request $request, $photoId)
|
||||||
|
{
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'caption' => 'required|string|max:255',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => $validator->errors()
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$photo = Photo::findOrFail($photoId);
|
||||||
|
$photo->caption = $request->input('caption');
|
||||||
|
$photo->save();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Caption updated successfully',
|
||||||
|
'data' => $photo
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a photo
|
||||||
|
*/
|
||||||
|
public function delete($photoId)
|
||||||
|
{
|
||||||
|
$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->setAsProfilePhoto();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the database record
|
||||||
|
$photo->delete();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Photo deleted successfully'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ use Spatie\Activitylog\LogOptions;
|
||||||
|
|
||||||
class Person extends Model
|
class Person extends Model
|
||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes, Searchable, LogsActivity;
|
use HasFactory, SoftDeletes, LogsActivity;
|
||||||
|
|
||||||
protected $table = 'person';
|
protected $table = 'person';
|
||||||
protected $primaryKey = 'person_id';
|
protected $primaryKey = 'person_id';
|
||||||
|
|
@ -80,15 +80,16 @@ class Person extends Model
|
||||||
return $this->hasOne(Internment::class, 'person_id');
|
return $this->hasOne(Internment::class, 'person_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toSearchableArray()
|
public function photos()
|
||||||
{
|
{
|
||||||
return [
|
return $this->hasMany(Photo::class, 'person_id', 'person_id');
|
||||||
'person_id' => $this->person_id,
|
}
|
||||||
'surname' => $this->surname,
|
|
||||||
'christian_name' => $this->christian_name,
|
public function profilePhoto()
|
||||||
'full_name' => $this->full_name,
|
{
|
||||||
'place_of_birth' => $this->place_of_birth,
|
return $this->hasMany(Photo::class, 'person_id', 'person_id')
|
||||||
'id_card_no' => $this->id_card_no,
|
->where('is_profile_photo', true)
|
||||||
];
|
->latest()
|
||||||
|
->first();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Spatie\Activitylog\Traits\LogsActivity;
|
||||||
|
use Spatie\Activitylog\LogOptions;
|
||||||
|
|
||||||
|
class Photo extends Model
|
||||||
|
{
|
||||||
|
use HasFactory, SoftDeletes, LogsActivity;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'person_id',
|
||||||
|
'filename',
|
||||||
|
'original_filename',
|
||||||
|
'file_path',
|
||||||
|
'mime_type',
|
||||||
|
'file_size',
|
||||||
|
'is_profile_photo',
|
||||||
|
'caption',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_profile_photo' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🔧 Configure Spatie logging
|
||||||
|
public function getActivitylogOptions(): LogOptions
|
||||||
|
{
|
||||||
|
return LogOptions::defaults()
|
||||||
|
->useLogName('photo')
|
||||||
|
->logFillable()
|
||||||
|
->logOnlyDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📝 Custom activity description for events
|
||||||
|
public function getDescriptionForEvent(string $eventName): string
|
||||||
|
{
|
||||||
|
return match ($eventName) {
|
||||||
|
'created' => 'Added new photo',
|
||||||
|
'updated' => 'Updated photo details',
|
||||||
|
'deleted' => 'Deleted photo',
|
||||||
|
default => ucfirst($eventName) . ' photo record',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔗 Relationships
|
||||||
|
public function person()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Person::class, 'person_id', 'person_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to set a photo as profile photo
|
||||||
|
public function setAsProfilePhoto(): bool
|
||||||
|
{
|
||||||
|
// First unset any existing profile photo for this person
|
||||||
|
if ($this->person_id) {
|
||||||
|
self::where('person_id', $this->person_id)
|
||||||
|
->where('is_profile_photo', true)
|
||||||
|
->update(['is_profile_photo' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set this photo as profile photo
|
||||||
|
$this->is_profile_photo = true;
|
||||||
|
return $this->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('photos', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('person_id');
|
||||||
|
$table->string('filename');
|
||||||
|
$table->string('original_filename')->nullable();
|
||||||
|
$table->string('file_path');
|
||||||
|
$table->string('mime_type')->nullable();
|
||||||
|
$table->integer('file_size')->nullable(); // in KB
|
||||||
|
$table->boolean('is_profile_photo')->default(false);
|
||||||
|
$table->text('caption')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
|
||||||
|
// Foreign key constraint
|
||||||
|
$table->foreign('person_id')
|
||||||
|
->references('person_id')
|
||||||
|
->on('person')
|
||||||
|
->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('photos');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -17,6 +17,7 @@ class DatabaseSeeder extends Seeder
|
||||||
$this->call([
|
$this->call([
|
||||||
AdminUserSeeder::class,
|
AdminUserSeeder::class,
|
||||||
PersonSeeder::class, // Seed 100 sample Person records
|
PersonSeeder::class, // Seed 100 sample Person records
|
||||||
|
PhotoSeeder::class, // Seed photos for the Person records
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Create a regular user for testing
|
// Create a regular user for testing
|
||||||
|
|
|
||||||
|
|
@ -65,10 +65,13 @@ class PersonSeeder extends Seeder
|
||||||
|
|
||||||
// Create Residence data (70% chance)
|
// Create Residence data (70% chance)
|
||||||
if ($faker->boolean(70)) {
|
if ($faker->boolean(70)) {
|
||||||
|
// Use only specific Northern Territory locations
|
||||||
|
$ntLocations = ['Darwin', 'Tennant Creek', 'Katherine', 'Alice Springs'];
|
||||||
|
|
||||||
Residence::create([
|
Residence::create([
|
||||||
'person_id' => $person->person_id,
|
'person_id' => $person->person_id,
|
||||||
'town_or_city' => $faker->city,
|
'town_or_city' => $faker->randomElement($ntLocations),
|
||||||
'home_at_death' => $isDeceased ? $faker->streetAddress . ', ' . $faker->city : null,
|
'home_at_death' => $isDeceased ? $faker->streetAddress . ', ' . $faker->randomElement($ntLocations) : null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Person;
|
||||||
|
use App\Models\Photo;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class PhotoSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Create the storage directory if it doesn't exist
|
||||||
|
if (!File::exists(storage_path('app/public/photos'))) {
|
||||||
|
File::makeDirectory(storage_path('app/public/photos'), 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample image URLs for testing
|
||||||
|
$sampleImages = [
|
||||||
|
'https://randomuser.me/api/portraits/men/1.jpg',
|
||||||
|
'https://randomuser.me/api/portraits/men/2.jpg',
|
||||||
|
'https://randomuser.me/api/portraits/men/3.jpg',
|
||||||
|
'https://randomuser.me/api/portraits/women/1.jpg',
|
||||||
|
'https://randomuser.me/api/portraits/women/2.jpg',
|
||||||
|
'https://randomuser.me/api/portraits/women/3.jpg',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Sample captions
|
||||||
|
$sampleCaptions = [
|
||||||
|
'Official ID photo',
|
||||||
|
'Family portrait',
|
||||||
|
'At work',
|
||||||
|
'Travel document photo',
|
||||||
|
'Residence permit photo',
|
||||||
|
'Personal photo',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get all person records
|
||||||
|
$persons = Person::all();
|
||||||
|
|
||||||
|
// Process each person
|
||||||
|
foreach ($persons as $person) {
|
||||||
|
// Skip some persons to have variety (30% chance to skip)
|
||||||
|
if (rand(1, 10) <= 3) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 1-3 photos for this person
|
||||||
|
$numPhotos = rand(1, 3);
|
||||||
|
$profilePhotoSet = false;
|
||||||
|
|
||||||
|
for ($i = 0; $i < $numPhotos; $i++) {
|
||||||
|
// Choose a random sample image
|
||||||
|
$imageUrl = $sampleImages[array_rand($sampleImages)];
|
||||||
|
$imageData = file_get_contents($imageUrl);
|
||||||
|
|
||||||
|
// Create directory for this person if it doesn't exist
|
||||||
|
$personDir = storage_path('app/public/photos/' . $person->person_id);
|
||||||
|
if (!File::exists($personDir)) {
|
||||||
|
File::makeDirectory($personDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a unique filename
|
||||||
|
$filename = Str::uuid() . '.jpg';
|
||||||
|
$fullPath = $personDir . '/' . $filename;
|
||||||
|
|
||||||
|
// Save the image file
|
||||||
|
file_put_contents($fullPath, $imageData);
|
||||||
|
|
||||||
|
// Choose if this should be a profile photo
|
||||||
|
// First photo has 70% chance, otherwise 0% chance
|
||||||
|
$isProfilePhoto = !$profilePhotoSet && (rand(1, 10) <= 7);
|
||||||
|
|
||||||
|
if ($isProfilePhoto) {
|
||||||
|
$profilePhotoSet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the photo record
|
||||||
|
$photo = new Photo([
|
||||||
|
'person_id' => $person->person_id,
|
||||||
|
'filename' => $filename,
|
||||||
|
'original_filename' => 'sample_' . rand(1000, 9999) . '.jpg',
|
||||||
|
'file_path' => '/storage/photos/' . $person->person_id . '/' . $filename,
|
||||||
|
'mime_type' => 'image/jpeg',
|
||||||
|
'file_size' => strlen($imageData) / 1024, // Convert to KB
|
||||||
|
'caption' => $sampleCaptions[array_rand($sampleCaptions)],
|
||||||
|
'is_profile_photo' => $isProfilePhoto
|
||||||
|
]);
|
||||||
|
|
||||||
|
$photo->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->command->info('Created photos for ' . Photo::count() . ' person records');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,40 +1,46 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use App\Http\Controllers\PersonController;
|
use App\Http\Controllers\MigrantController;
|
||||||
use App\Http\Controllers\AuthController;
|
use App\Http\Controllers\AuthController;
|
||||||
use App\Http\Controllers\HistoricalSearchController;
|
use App\Http\Controllers\HistoricalSearchController;
|
||||||
use App\Http\Controllers\DashboardController;
|
use App\Http\Controllers\DashboardController;
|
||||||
use App\Http\Controllers\ActivityLogController;
|
use App\Http\Controllers\ActivityLogController;
|
||||||
|
|
||||||
|
// Public routes
|
||||||
Route::post('/login', [AuthController::class, 'login'])->name('login');
|
Route::post('/login', [AuthController::class, 'login'])->name('login');
|
||||||
Route::post('/register', [AuthController::class, 'register'])->name('register');
|
Route::post('/register', [AuthController::class, 'register'])->name('register');
|
||||||
|
Route::get('/migrants', [MigrantController::class, 'index']);
|
||||||
|
Route::get('/migrants/{id}', [MigrantController::class, 'show']);
|
||||||
|
Route::get('/migrants/{id}/photos', [MigrantController::class, 'getPhotos']);
|
||||||
|
|
||||||
|
|
||||||
Route::prefix('historical')->group(function () {
|
|
||||||
Route::get('search', [HistoricalSearchController::class, 'search']);
|
|
||||||
Route::get('record/{id}', [HistoricalSearchController::class, 'getRecord']);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Protected routes - require Sanctum authentication
|
// Protected routes - require Sanctum authentication
|
||||||
Route::middleware('auth:sanctum')->group(function () {
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
// User authentication routes
|
|
||||||
Route::get('/user', [AuthController::class, 'me'])->name('user.profile');
|
|
||||||
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
|
|
||||||
Route::get('/dashboard/stats', [DashboardController::class, 'getStats']);
|
|
||||||
// Admin-only routes
|
|
||||||
// Route::middleware('ability:admin')->group(function () {
|
|
||||||
// Route::post('/register', [AuthController::class, 'register'])->name('register');
|
|
||||||
// });
|
|
||||||
|
|
||||||
Route::get('/persons/search', [PersonController::class, 'search']);
|
// User routes
|
||||||
Route::get('/persons/{id}', [PersonController::class, 'show']);
|
Route::get('/user', [AuthController::class, 'me'])->name('user.profile');
|
||||||
|
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
|
||||||
|
|
||||||
Route::get('/activity-logs', [ActivityLogController::class, 'index']);
|
// Dashboard routes
|
||||||
// Person API endpoints - all CRUD operations protected by authentication
|
Route::get('/dashboard/stats', [DashboardController::class, 'getStats']);
|
||||||
Route::apiResource('persons', PersonController::class);
|
Route::get('/activity-logs', [ActivityLogController::class, 'index']);
|
||||||
|
|
||||||
|
// Migrant CRUD routes (now includes photo uploads)
|
||||||
|
Route::post('/migrants', [MigrantController::class, 'store']); // Can include photos
|
||||||
|
Route::put('/migrants/{id}', [MigrantController::class, 'update']); // Can include photos
|
||||||
|
Route::patch('/migrants/{id}', [MigrantController::class, 'update']); // Can include photos
|
||||||
|
Route::delete('/migrants/{id}', [MigrantController::class, 'destroy']); // Deletes photos too
|
||||||
|
|
||||||
|
// Photo management routes (all handled by MigrantController)
|
||||||
|
Route::post('/migrants/{id}/photos', [MigrantController::class, 'uploadPhotos']);
|
||||||
|
Route::post('/migrants/photos/{photoId}/set-as-profile', [MigrantController::class, 'setAsProfilePhoto']);
|
||||||
|
Route::put('/migrants/photos/{photoId}/caption', [MigrantController::class, 'updatePhotoCaption']);
|
||||||
|
Route::delete('/migrants/photos/{photoId}', [MigrantController::class, 'deletePhoto']);
|
||||||
|
|
||||||
// Custom route for finding a person by ID card number
|
|
||||||
// Route::get('persons/id-card/{idCardNo}', [PersonController::class, 'findByIdCard'])->name('persons.findByIdCard');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Admin-only routes
|
||||||
|
// Route::middleware('ability:admin')->group(function () {
|
||||||
|
// Route::post('/register', [AuthController::class, 'register'])->name('register');
|
||||||
|
// });
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Person;
|
||||||
|
use App\Models\Photo;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Foundation\Testing\WithFaker;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class MigrantPhotoTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase, WithFaker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up authentication for each test
|
||||||
|
*/
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Create and authenticate as admin user for all tests
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'email' => 'test.admin@example.com',
|
||||||
|
'is_admin' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
Sanctum::actingAs($user, ['*']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test creating a migrant with multiple photos and setting one as profile.
|
||||||
|
*/
|
||||||
|
public function test_can_create_migrant_with_multiple_photos_and_set_profile(): void
|
||||||
|
{
|
||||||
|
// Create fake storage disk for testing
|
||||||
|
Storage::fake('public');
|
||||||
|
|
||||||
|
// Create test photo files (using create instead of image to avoid GD dependency)
|
||||||
|
$photo1 = UploadedFile::fake()->create('photo1.jpg', 100, 'image/jpeg');
|
||||||
|
$photo2 = UploadedFile::fake()->create('photo2.jpg', 100, 'image/jpeg');
|
||||||
|
$photo3 = UploadedFile::fake()->create('photo3.jpg', 100, 'image/jpeg');
|
||||||
|
|
||||||
|
// Create migrant data
|
||||||
|
$migrantData = [
|
||||||
|
'surname' => $this->faker->lastName,
|
||||||
|
'christian_name' => $this->faker->firstName,
|
||||||
|
'date_of_birth' => $this->faker->date(),
|
||||||
|
'place_of_birth' => $this->faker->city,
|
||||||
|
'occupation' => $this->faker->jobTitle,
|
||||||
|
'id_card_no' => (string)$this->faker->unique()->randomNumber(6),
|
||||||
|
'migration' => [
|
||||||
|
'date_of_arrival_aus' => $this->faker->date(),
|
||||||
|
'date_of_arrival_nt' => $this->faker->date(),
|
||||||
|
'arrival_period' => '1950-1960',
|
||||||
|
],
|
||||||
|
// Add photos to the request
|
||||||
|
'photos' => [$photo1, $photo2, $photo3],
|
||||||
|
'captions' => ['First photo', 'Second photo', 'Third photo'],
|
||||||
|
'set_as_profile' => 1, // Set the first photo as profile
|
||||||
|
];
|
||||||
|
|
||||||
|
// Make the API request to create a migrant with photos
|
||||||
|
$response = $this->postJson('/api/migrants', $migrantData);
|
||||||
|
|
||||||
|
// Assert the response is successful
|
||||||
|
$response->assertStatus(201)
|
||||||
|
->assertJsonStructure([
|
||||||
|
'success',
|
||||||
|
'data' => [
|
||||||
|
'person',
|
||||||
|
'uploaded_photos',
|
||||||
|
],
|
||||||
|
'message',
|
||||||
|
])
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Person created successfully',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get the created person ID
|
||||||
|
$personId = $response->json('data.person.person_id');
|
||||||
|
|
||||||
|
// Assert that the person was created in the database
|
||||||
|
$this->assertDatabaseHas('person', [
|
||||||
|
'surname' => $migrantData['surname'],
|
||||||
|
'christian_name' => $migrantData['christian_name'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert that 3 photos were created for this person
|
||||||
|
$this->assertEquals(3, Photo::where('person_id', $personId)->count());
|
||||||
|
|
||||||
|
// Assert that exactly one photo is set as profile
|
||||||
|
$this->assertEquals(1, Photo::where('person_id', $personId)
|
||||||
|
->where('is_profile_photo', true)
|
||||||
|
->count());
|
||||||
|
|
||||||
|
// Get the profile photo
|
||||||
|
$profilePhoto = Photo::where('person_id', $personId)
|
||||||
|
->where('is_profile_photo', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
// Assert that the first photo is the profile (based on caption)
|
||||||
|
$this->assertEquals('First photo', $profilePhoto->caption);
|
||||||
|
|
||||||
|
// Assert that the files were stored in the storage
|
||||||
|
foreach (Photo::where('person_id', $personId)->get() as $photo) {
|
||||||
|
Storage::disk('public')->assertExists('photos/' . $personId . '/' . $photo->filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test updating the profile photo of an existing migrant.
|
||||||
|
*/
|
||||||
|
public function test_can_change_profile_photo(): void
|
||||||
|
{
|
||||||
|
// Create fake storage disk for testing
|
||||||
|
Storage::fake('public');
|
||||||
|
|
||||||
|
// Create a migrant
|
||||||
|
$person = Person::factory()->create();
|
||||||
|
|
||||||
|
// Create three photos for this migrant
|
||||||
|
$photos = [];
|
||||||
|
$captions = ['Photo 1', 'Photo 2', 'Photo 3'];
|
||||||
|
|
||||||
|
for ($i = 0; $i < 3; $i++) {
|
||||||
|
$filename = "photo{$i}.jpg";
|
||||||
|
$file = UploadedFile::fake()->create($filename, 100, 'image/jpeg');
|
||||||
|
|
||||||
|
// Upload the file to the storage
|
||||||
|
$path = $file->storeAs("photos/{$person->person_id}", $filename, 'public');
|
||||||
|
|
||||||
|
// Create the photo record
|
||||||
|
$photo = Photo::create([
|
||||||
|
'person_id' => $person->person_id,
|
||||||
|
'filename' => $filename,
|
||||||
|
'original_filename' => $filename,
|
||||||
|
'file_path' => Storage::url($path),
|
||||||
|
'mime_type' => 'image/jpeg',
|
||||||
|
'file_size' => $file->getSize() / 1024,
|
||||||
|
'caption' => $captions[$i],
|
||||||
|
'is_profile_photo' => $i === 0 // Make the first one the profile initially
|
||||||
|
]);
|
||||||
|
|
||||||
|
$photos[] = $photo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now try to set the second photo as the profile
|
||||||
|
$response = $this->postJson("/api/migrants/photos/{$photos[1]->id}/set-as-profile");
|
||||||
|
|
||||||
|
// Assert response is successful
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Profile photo set successfully',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert that now the second photo is the profile
|
||||||
|
$this->assertDatabaseHas('photos', [
|
||||||
|
'id' => $photos[1]->id,
|
||||||
|
'is_profile_photo' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert that the first photo is no longer the profile
|
||||||
|
$this->assertDatabaseHas('photos', [
|
||||||
|
'id' => $photos[0]->id,
|
||||||
|
'is_profile_photo' => false
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert that only one photo is set as profile
|
||||||
|
$this->assertEquals(1, Photo::where('person_id', $person->person_id)
|
||||||
|
->where('is_profile_photo', true)
|
||||||
|
->count());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue