Api fix remove Resources
This commit is contained in:
parent
7925015da1
commit
6848722da3
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
class ActivityLogController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$logs = Activity::with('causer')
|
||||
->latest()
|
||||
->take(10)
|
||||
->get()
|
||||
->map(function ($log) {
|
||||
return [
|
||||
'log_name' => $log->log_name,
|
||||
'description' => $log->description,
|
||||
'causer_name' => optional($log->causer)->name ?? 'System',
|
||||
'subject_id' => $log->subject_id,
|
||||
'created_at' => $log->created_at->toDateTimeString(),
|
||||
'updated_at' => $log->updated_at->toDateTimeString(),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $logs,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,33 +12,26 @@ use Illuminate\Validation\ValidationException;
|
|||
class AuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* Register a new user (Admin only)
|
||||
* Register a new user
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function register(Request $request): JsonResponse
|
||||
{
|
||||
// Only allow admins to register new users
|
||||
if (!auth()->check() || !auth()->user()->is_admin) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized action'
|
||||
], 403);
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'password' => 'required|string|min:8',
|
||||
'is_admin' => 'boolean'
|
||||
// 'is_admin' => 'boolean'
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
'is_admin' => $request->is_admin ?? false,
|
||||
// 'is_admin' => $request->is_admin ?? false,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Person;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function getStats()
|
||||
{
|
||||
// Total migrants count
|
||||
$totalMigrants = Person::count();
|
||||
|
||||
// New migrants in the current month
|
||||
$currentMonthStart = Carbon::now()->startOfMonth();
|
||||
$newThisMonth = Person::where('created_at', '>=', $currentMonthStart)->count();
|
||||
|
||||
// Recent additions (last 30 days)
|
||||
$thirtyDaysAgo = Carbon::now()->subDays(30);
|
||||
$recentAdditions = Person::where('created_at', '>=', $thirtyDaysAgo)->count();
|
||||
|
||||
// Pending reviews - example: people with missing information
|
||||
$pendingReviews = Person::whereNull('date_of_birth')
|
||||
->orWhereNull('place_of_birth')
|
||||
->orWhereNull('occupation')
|
||||
->count();
|
||||
|
||||
// Incomplete records - persons missing multiple key fields
|
||||
$incompleteRecords = Person::where(function($query) {
|
||||
$query->whereNull('date_of_birth')
|
||||
->orWhereNull('place_of_birth');
|
||||
})
|
||||
->where(function($query) {
|
||||
$query->whereNull('occupation')
|
||||
->orWhereNull('reference')
|
||||
->orWhereNull('id_card_no');
|
||||
})
|
||||
->count();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'total_migrants' => $totalMigrants,
|
||||
'new_this_month' => $newThisMonth,
|
||||
'recent_additions' => $recentAdditions,
|
||||
'pending_reviews' => $pendingReviews,
|
||||
'incomplete_records' => $incompleteRecords
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,309 +4,172 @@ namespace App\Http\Controllers;
|
|||
|
||||
use App\Http\Requests\StorePersonRequest;
|
||||
use App\Http\Requests\UpdatePersonRequest;
|
||||
use App\Http\Resources\PersonResource;
|
||||
use App\Http\Resources\PersonCollection;
|
||||
use App\Models\Person;
|
||||
use App\Models\Migration;
|
||||
use App\Models\Naturalization;
|
||||
use App\Models\Residence;
|
||||
use App\Models\Family;
|
||||
use App\Models\Internment;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
|
||||
class PersonController extends Controller
|
||||
{
|
||||
|
||||
// Public search functionality has been moved to PublicSearchController
|
||||
|
||||
public function getRecord($id): JsonResponse
|
||||
{
|
||||
try {
|
||||
try {
|
||||
$person = Person::with([
|
||||
'migration',
|
||||
'naturalization',
|
||||
'residence',
|
||||
'family',
|
||||
'internment'
|
||||
])->findOrFail($id);
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Record not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => new PersonResource($person),
|
||||
'message' => 'Record retrieved successfully'
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to retrieve record',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
protected array $relations = ['migration', 'naturalization', 'residence', 'family', 'internment'];
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$query = Person::query();
|
||||
$query = Person::with($this->relations);
|
||||
|
||||
// Apply search filters if provided
|
||||
if ($request->has('search')) {
|
||||
$searchTerm = $request->search;
|
||||
$query->where(function($q) use ($searchTerm) {
|
||||
$q->where('full_name', 'LIKE', "%{$searchTerm}%")
|
||||
->orWhere('surname', 'LIKE', "%{$searchTerm}%")
|
||||
->orWhere('occupation', 'LIKE', "%{$searchTerm}%");
|
||||
});
|
||||
if ($search = $request->search) {
|
||||
$query->where(fn($q) =>
|
||||
$q->where('full_name', 'LIKE', "%$search%")
|
||||
->orWhere('surname', 'LIKE', "%$search%")
|
||||
->orWhere('occupation', 'LIKE', "%$search%")
|
||||
);
|
||||
}
|
||||
|
||||
// Paginate results
|
||||
$persons = $query->paginate(10);
|
||||
|
||||
// Eager load related models for each person in the collection
|
||||
$persons->getCollection()->each->load([
|
||||
'migration',
|
||||
'naturalization',
|
||||
'residence',
|
||||
'family',
|
||||
'internment'
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => new PersonCollection($persons),
|
||||
'data' => $query->paginate(10),
|
||||
'message' => 'Persons retrieved successfully'
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to retrieve persons',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function store(StorePersonRequest $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
// Use DB transaction for atomic operations
|
||||
$result = DB::transaction(function () use ($request) {
|
||||
// Create person record
|
||||
$person = Person::create($request->only([
|
||||
'surname', 'christian_name', 'full_name', 'date_of_birth',
|
||||
'place_of_birth', 'date_of_death', 'occupation',
|
||||
'additional_notes', 'reference', 'id_card_no'
|
||||
]));
|
||||
|
||||
// Create migration record if data is provided
|
||||
if ($request->has('migration')) {
|
||||
$person->migration()->create($request->migration);
|
||||
}
|
||||
|
||||
// Create naturalization record if data is provided
|
||||
if ($request->has('naturalization')) {
|
||||
$person->naturalization()->create($request->naturalization);
|
||||
}
|
||||
|
||||
// Create residence record if data is provided
|
||||
if ($request->has('residence')) {
|
||||
$person->residence()->create($request->residence);
|
||||
}
|
||||
|
||||
// Create family record if data is provided
|
||||
if ($request->has('family')) {
|
||||
$person->family()->create($request->family);
|
||||
}
|
||||
|
||||
// Create internment record if data is provided
|
||||
if ($request->has('internment')) {
|
||||
$person->internment()->create($request->internment);
|
||||
}
|
||||
|
||||
// Load all relationships for the response
|
||||
$person->load(['migration', 'naturalization', 'residence', 'family', 'internment']);
|
||||
|
||||
return $person;
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => new PersonResource($result),
|
||||
'message' => 'Person created successfully'
|
||||
], 201);
|
||||
} catch (Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to create person',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
return $this->errorResponse('Failed to retrieve persons', $e);
|
||||
}
|
||||
}
|
||||
|
||||
public function show(string $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
// Find person by ID and eager load all relationships
|
||||
$person = Person::with(['migration', 'naturalization', 'residence', 'family', 'internment'])
|
||||
->find($id);
|
||||
$person = Person::with($this->relations)->find($id);
|
||||
|
||||
if (!$person) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Person not found'
|
||||
], 404);
|
||||
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 response()->json([
|
||||
'success' => true,
|
||||
'data' => new PersonResource($person),
|
||||
'message' => 'Person retrieved successfully'
|
||||
]);
|
||||
return $this->successResponse(
|
||||
$person->load($this->relations),
|
||||
'Person created successfully',
|
||||
201
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to retrieve person',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
return $this->errorResponse('Failed to create person', $e);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(UpdatePersonRequest $request, string $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
// Use DB transaction for atomic operations
|
||||
$result = DB::transaction(function () use ($request, $id) {
|
||||
// Find person by ID
|
||||
$person = Person::findOrFail($id);
|
||||
$person = Person::findOrFail($id);
|
||||
|
||||
// Update person record
|
||||
$person->update($request->only([
|
||||
'surname', 'christian_name', 'full_name', 'date_of_birth',
|
||||
'place_of_birth', 'date_of_death', 'occupation',
|
||||
'additional_notes', 'reference', 'id_card_no'
|
||||
]));
|
||||
|
||||
// Update migration record if data is provided
|
||||
if ($request->has('migration')) {
|
||||
if ($person->migration) {
|
||||
$person->migration->update($request->migration);
|
||||
} else {
|
||||
$person->migration()->create($request->migration);
|
||||
}
|
||||
}
|
||||
|
||||
// Update naturalization record if data is provided
|
||||
if ($request->has('naturalization')) {
|
||||
if ($person->naturalization) {
|
||||
$person->naturalization->update($request->naturalization);
|
||||
} else {
|
||||
$person->naturalization()->create($request->naturalization);
|
||||
}
|
||||
}
|
||||
|
||||
// Update residence record if data is provided
|
||||
if ($request->has('residence')) {
|
||||
if ($person->residence) {
|
||||
$person->residence->update($request->residence);
|
||||
} else {
|
||||
$person->residence()->create($request->residence);
|
||||
}
|
||||
}
|
||||
|
||||
// Update family record if data is provided
|
||||
if ($request->has('family')) {
|
||||
if ($person->family) {
|
||||
$person->family->update($request->family);
|
||||
} else {
|
||||
$person->family()->create($request->family);
|
||||
}
|
||||
}
|
||||
|
||||
// Update internment record if data is provided
|
||||
if ($request->has('internment')) {
|
||||
if ($person->internment) {
|
||||
$person->internment->update($request->internment);
|
||||
} else {
|
||||
$person->internment()->create($request->internment);
|
||||
}
|
||||
}
|
||||
|
||||
// Load all relationships for the response
|
||||
$person->load(['migration', 'naturalization', 'residence', 'family', 'internment']);
|
||||
|
||||
return $person;
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => new PersonResource($result),
|
||||
'message' => 'Person updated successfully'
|
||||
$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 response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to update person',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
return $this->errorResponse('Failed to update person', $e);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy(string $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
// Use DB transaction for atomic operations
|
||||
DB::transaction(function () use ($id) {
|
||||
// Find person by ID with related models
|
||||
$person = Person::with([
|
||||
'migration', 'naturalization', 'residence', 'family', 'internment'
|
||||
])->findOrFail($id);
|
||||
$person = Person::with($this->relations)->findOrFail($id);
|
||||
|
||||
// Manually delete each related model to ensure soft deletes work correctly
|
||||
if ($person->migration) {
|
||||
$person->migration->delete();
|
||||
}
|
||||
foreach ($this->relations as $relation) {
|
||||
$person->$relation?->delete();
|
||||
}
|
||||
|
||||
if ($person->naturalization) {
|
||||
$person->naturalization->delete();
|
||||
}
|
||||
$person->delete();
|
||||
|
||||
if ($person->residence) {
|
||||
$person->residence->delete();
|
||||
}
|
||||
|
||||
if ($person->family) {
|
||||
$person->family->delete();
|
||||
}
|
||||
|
||||
if ($person->internment) {
|
||||
$person->internment->delete();
|
||||
}
|
||||
|
||||
// Now delete the person record
|
||||
$person->delete();
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Person deleted successfully'
|
||||
]);
|
||||
return $this->successResponse(null, 'Person deleted successfully');
|
||||
} catch (Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to delete person',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,290 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Resources\PersonCollection;
|
||||
use App\Models\Person;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Exception;
|
||||
|
||||
class PublicSearchController extends Controller
|
||||
{
|
||||
/**
|
||||
* Search for persons with various filters
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function search(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$query = $this->buildSearchQuery($request);
|
||||
$persons = $this->paginateAndLoadRelations($query, $request);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => new PersonCollection($persons),
|
||||
'message' => 'Public search results retrieved successfully'
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to retrieve search results',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the search query with all filters applied
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Builder
|
||||
*/
|
||||
private function buildSearchQuery(Request $request): Builder
|
||||
{
|
||||
$query = Person::query();
|
||||
$useOrConditions = $this->shouldUseOrConditions($request);
|
||||
|
||||
// Apply basic filters (firstName, lastName, etc.)
|
||||
$this->applyBasicFilters($query, $request, $useOrConditions);
|
||||
|
||||
// Apply relation-based filters
|
||||
$this->applyYearOfArrivalFilter($query, $request);
|
||||
$this->applyAgeAtMigrationFilter($query, $request);
|
||||
$this->applySettlementLocationFilter($query, $request);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if OR logic should be used between filters
|
||||
*
|
||||
* @param Request $request
|
||||
* @return bool
|
||||
*/
|
||||
private function shouldUseOrConditions(Request $request): bool
|
||||
{
|
||||
$exactMatch = $request->boolean('exactMatch');
|
||||
$useOrLogic = $request->boolean('useOrLogic');
|
||||
|
||||
return $useOrLogic || $exactMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters for fields in the Person table
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param Request $request
|
||||
* @param bool $useOrConditions
|
||||
* @return void
|
||||
*/
|
||||
private function applyBasicFilters(Builder $query, Request $request, bool $useOrConditions): void
|
||||
{
|
||||
$filters = $this->collectBasicFilters($request);
|
||||
|
||||
if (empty($filters)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($useOrConditions) {
|
||||
$this->applyFiltersWithOrLogic($query, $filters);
|
||||
} else {
|
||||
$this->applyFiltersWithAndLogic($query, $filters);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect basic filters from the request
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
private function collectBasicFilters(Request $request): array
|
||||
{
|
||||
$exactMatch = $request->boolean('exactMatch');
|
||||
$filters = [];
|
||||
|
||||
$filterFields = [
|
||||
'id_card_no' => 'id_card_no',
|
||||
'firstName' => 'christian_name',
|
||||
'lastName' => 'surname',
|
||||
'regionOfOrigin' => 'place_of_birth'
|
||||
];
|
||||
|
||||
foreach ($filterFields as $requestKey => $dbField) {
|
||||
if ($request->has($requestKey) && $request->input($requestKey) !== 'all') {
|
||||
$value = $request->input($requestKey);
|
||||
|
||||
if ($exactMatch) {
|
||||
// For exact matching, use raw query with BINARY for strict case-sensitive matching
|
||||
$filters[] = [$dbField, 'raw', $value];
|
||||
} else {
|
||||
// For partial matching, use standard LIKE with wildcards
|
||||
$filters[] = [$dbField, 'LIKE', "%{$value}%"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters using OR logic
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param array $filters
|
||||
* @return void
|
||||
*/
|
||||
private function applyFiltersWithOrLogic(Builder $query, array $filters): void
|
||||
{
|
||||
$query->where(function ($q) use ($filters) {
|
||||
foreach ($filters as $index => $filter) {
|
||||
$method = $index === 0 ? 'where' : 'orWhere';
|
||||
|
||||
if ($filter[1] === 'raw') {
|
||||
// Handle raw exact matching (case-sensitive)
|
||||
$field = $filter[0];
|
||||
$value = $filter[2];
|
||||
$q->whereRaw("BINARY {$field} = ?", [$value]);
|
||||
} else {
|
||||
// Handle standard operators
|
||||
$q->{$method}($filter[0], $filter[1], $filter[2]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters using AND logic
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param array $filters
|
||||
* @return void
|
||||
*/
|
||||
private function applyFiltersWithAndLogic(Builder $query, array $filters): void
|
||||
{
|
||||
foreach ($filters as $filter) {
|
||||
if ($filter[1] === 'raw') {
|
||||
// Handle raw exact matching (case-sensitive)
|
||||
$field = $filter[0];
|
||||
$value = $filter[2];
|
||||
$query->whereRaw("BINARY {$field} = ?", [$value]);
|
||||
} else {
|
||||
// Handle standard operators
|
||||
$query->where($filter[0], $filter[1], $filter[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by Year of Arrival
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param Request $request
|
||||
* @return void
|
||||
*/
|
||||
private function applyYearOfArrivalFilter(Builder $query, Request $request): void
|
||||
{
|
||||
if ($request->has('yearOfArrival') && $request->yearOfArrival !== 'all') {
|
||||
$year = $request->yearOfArrival;
|
||||
$query->whereHas('migration', function (Builder $subQuery) use ($year) {
|
||||
$subQuery->whereYear('date_of_arrival_aus', $year)
|
||||
->orWhereYear('date_of_arrival_nt', $year);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by Age at Migration
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param Request $request
|
||||
* @return void
|
||||
*/
|
||||
private function applyAgeAtMigrationFilter(Builder $query, Request $request): void
|
||||
{
|
||||
if ($request->has('ageAtMigration') && $request->ageAtMigration !== 'all') {
|
||||
$ageAtMigration = (int) $request->ageAtMigration;
|
||||
|
||||
$query->whereHas('migration', function (Builder $subQuery) use ($ageAtMigration) {
|
||||
$subQuery->whereRaw('YEAR(date_of_arrival_aus) - YEAR(person.date_of_birth) = ?', [$ageAtMigration])
|
||||
->orWhereRaw('YEAR(date_of_arrival_nt) - YEAR(person.date_of_birth) = ?', [$ageAtMigration]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by Settlement Location
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param Request $request
|
||||
* @return void
|
||||
*/
|
||||
private function applySettlementLocationFilter(Builder $query, Request $request): void
|
||||
{
|
||||
if ($request->has('settlementLocation') && $request->settlementLocation !== 'all') {
|
||||
$location = $request->settlementLocation;
|
||||
$query->whereHas('residence', function (Builder $subQuery) use ($location) {
|
||||
$subQuery->where('town_or_city', 'LIKE', "%{$location}%");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginate results and load related models
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Pagination\LengthAwarePaginator
|
||||
*/
|
||||
private function paginateAndLoadRelations(Builder $query, Request $request): \Illuminate\Pagination\LengthAwarePaginator
|
||||
{
|
||||
$perPage = $request->input('per_page', 10);
|
||||
$persons = $query->paginate($perPage);
|
||||
|
||||
// Eager load related models
|
||||
$persons->getCollection()->each->load([
|
||||
'migration',
|
||||
'naturalization',
|
||||
'residence',
|
||||
'family',
|
||||
'internment'
|
||||
]);
|
||||
|
||||
return $persons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific migrant record by ID
|
||||
*
|
||||
* @param mixed $id
|
||||
* @return JsonResponse
|
||||
*/
|
||||
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) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Record not found',
|
||||
'error' => $e->getMessage()
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class FamilyResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return $this->resource ? [
|
||||
'family_id' => $this->family_id,
|
||||
'person_id' => $this->person_id,
|
||||
'names_of_parents' => $this->names_of_parents,
|
||||
'names_of_children' => $this->names_of_children,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
] : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class InternmentResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return $this->resource ? [
|
||||
'internment_id' => $this->internment_id,
|
||||
'person_id' => $this->person_id,
|
||||
'corps_issued' => $this->corps_issued,
|
||||
'interned_in' => $this->interned_in,
|
||||
'sent_to' => $this->sent_to,
|
||||
'internee_occupation' => $this->internee_occupation,
|
||||
'internee_address' => $this->internee_address,
|
||||
'cav' => $this->cav,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
] : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class MigrationResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return $this->resource ? [
|
||||
'migration_id' => $this->migration_id,
|
||||
'person_id' => $this->person_id,
|
||||
'date_of_arrival_aus' => $this->date_of_arrival_aus,
|
||||
'date_of_arrival_nt' => $this->date_of_arrival_nt,
|
||||
'arrival_period' => $this->arrival_period,
|
||||
'data_source' => $this->data_source,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
] : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class NaturalizationResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return $this->resource ? [
|
||||
'naturalization_id' => $this->naturalization_id,
|
||||
'person_id' => $this->person_id,
|
||||
'date_of_naturalisation' => $this->date_of_naturalisation,
|
||||
'no_of_cert' => $this->no_of_cert,
|
||||
'issued_at' => $this->issued_at,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
] : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class PersonCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @return array<int|string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'data' => $this->collection->map(function ($person) {
|
||||
return new PersonResource($person);
|
||||
}),
|
||||
'meta' => [
|
||||
'total' => $this->total(),
|
||||
'count' => $this->count(),
|
||||
'per_page' => $this->perPage(),
|
||||
'current_page' => $this->currentPage(),
|
||||
'last_page' => $this->lastPage(),
|
||||
],
|
||||
'links' => [
|
||||
'first' => $this->url(1),
|
||||
'last' => $this->url($this->lastPage()),
|
||||
'prev' => $this->previousPageUrl(),
|
||||
'next' => $this->nextPageUrl(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class PersonResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'person_id' => $this->person_id,
|
||||
'surname' => $this->surname,
|
||||
'christian_name' => $this->christian_name,
|
||||
'full_name' => $this->full_name,
|
||||
'date_of_birth' => $this->date_of_birth,
|
||||
'place_of_birth' => $this->place_of_birth,
|
||||
'date_of_death' => $this->date_of_death,
|
||||
'occupation' => $this->occupation,
|
||||
'additional_notes' => $this->additional_notes,
|
||||
'reference' => $this->reference,
|
||||
'id_card_no' => $this->id_card_no,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
|
||||
// Include related resources when they're loaded
|
||||
'migration' => new MigrationResource($this->whenLoaded('migration')),
|
||||
'naturalization' => new NaturalizationResource($this->whenLoaded('naturalization')),
|
||||
'residence' => new ResidenceResource($this->whenLoaded('residence')),
|
||||
'family' => new FamilyResource($this->whenLoaded('family')),
|
||||
'internment' => new InternmentResource($this->whenLoaded('internment')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ResidenceResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return $this->resource ? [
|
||||
'residence_id' => $this->residence_id,
|
||||
'person_id' => $this->person_id,
|
||||
'town_or_city' => $this->town_or_city,
|
||||
'home_at_death' => $this->home_at_death,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
] : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
// app/Models/Activity.php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Spatie\Activitylog\Models\Activity as SpatieActivity;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Activity extends SpatieActivity
|
||||
{
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'causer_id');
|
||||
}
|
||||
|
||||
// Optional: shortcut
|
||||
public function getCauserNameAttribute(): ?string
|
||||
{
|
||||
return $this->user?->name;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,10 +5,13 @@ namespace App\Models;
|
|||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
|
||||
class Person extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use HasFactory, SoftDeletes, Searchable, LogsActivity;
|
||||
|
||||
protected $table = 'person';
|
||||
protected $primaryKey = 'person_id';
|
||||
|
|
@ -31,7 +34,27 @@ class Person extends Model
|
|||
'date_of_death' => 'date',
|
||||
];
|
||||
|
||||
// Relationships
|
||||
// 🔧 Configure Spatie logging
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->useLogName('person')
|
||||
->logFillable()
|
||||
->logOnlyDirty();
|
||||
}
|
||||
|
||||
// 📝 Custom activity description for events
|
||||
public function getDescriptionForEvent(string $eventName): string
|
||||
{
|
||||
return match ($eventName) {
|
||||
'created' => 'Added new migrant',
|
||||
'updated' => 'Updated migrant details',
|
||||
'deleted' => 'Deleted migrant record',
|
||||
default => ucfirst($eventName) . ' migrant record',
|
||||
};
|
||||
}
|
||||
|
||||
// 🔗 Relationships
|
||||
public function migration()
|
||||
{
|
||||
return $this->hasOne(Migration::class, 'person_id');
|
||||
|
|
@ -56,4 +79,16 @@ class Person extends Model
|
|||
{
|
||||
return $this->hasOne(Internment::class, 'person_id');
|
||||
}
|
||||
|
||||
public function toSearchableArray()
|
||||
{
|
||||
return [
|
||||
'person_id' => $this->person_id,
|
||||
'surname' => $this->surname,
|
||||
'christian_name' => $this->christian_name,
|
||||
'full_name' => $this->full_name,
|
||||
'place_of_birth' => $this->place_of_birth,
|
||||
'id_card_no' => $this->id_card_no,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,22 +2,17 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
use HasApiTokens, HasFactory, Notifiable, LogsActivity;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
|
|
@ -25,21 +20,11 @@ class User extends Authenticatable
|
|||
'is_admin',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
|
@ -47,4 +32,13 @@ class User extends Authenticatable
|
|||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
// ✅ Required by LogsActivity
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->useLogName('user')
|
||||
->logOnly(['name', 'email', 'is_admin']) // specify what to track
|
||||
->logOnlyDirty(); // only log changes
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
|
@ -19,6 +20,11 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
Activity::saving(function ($activity) {
|
||||
if (auth()->check()) {
|
||||
$activity->causer_name = auth()->user()->name;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@
|
|||
"php": "^8.2",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/sanctum": "^4.1",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
"laravel/scout": "^10.15",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"protonemedia/laravel-cross-eloquent-search": "^3.6",
|
||||
"spatie/laravel-activitylog": "^4.10"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "015e02e23a490fcf9d86230851cba120",
|
||||
"content-hash": "1dfb34123f9a766e68760fa85b831c55",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
|
|
@ -1392,6 +1392,87 @@
|
|||
},
|
||||
"time": "2025-04-23T13:03:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/scout",
|
||||
"version": "v10.15.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/scout.git",
|
||||
"reference": "102fe09ae1c045c6f9cf1b3a2234e1fadb2198f2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/scout/zipball/102fe09ae1c045c6f9cf1b3a2234e1fadb2198f2",
|
||||
"reference": "102fe09ae1c045c6f9cf1b3a2234e1fadb2198f2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/bus": "^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/contracts": "^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/database": "^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/http": "^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/pagination": "^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/queue": "^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/support": "^9.0|^10.0|^11.0|^12.0",
|
||||
"php": "^8.0",
|
||||
"symfony/console": "^6.0|^7.0"
|
||||
},
|
||||
"conflict": {
|
||||
"algolia/algoliasearch-client-php": "<3.2.0|>=5.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"algolia/algoliasearch-client-php": "^3.2|^4.0",
|
||||
"meilisearch/meilisearch-php": "^1.0",
|
||||
"mockery/mockery": "^1.0",
|
||||
"orchestra/testbench": "^7.31|^8.11|^9.0|^10.0",
|
||||
"php-http/guzzle7-adapter": "^1.0",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpunit/phpunit": "^9.3|^10.4",
|
||||
"typesense/typesense-php": "^4.9.3"
|
||||
},
|
||||
"suggest": {
|
||||
"algolia/algoliasearch-client-php": "Required to use the Algolia engine (^3.2).",
|
||||
"meilisearch/meilisearch-php": "Required to use the Meilisearch engine (^1.0).",
|
||||
"typesense/typesense-php": "Required to use the Typesense engine (^4.9)."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Scout\\ScoutServiceProvider"
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "10.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Scout\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "Laravel Scout provides a driver based solution to searching your Eloquent models.",
|
||||
"keywords": [
|
||||
"algolia",
|
||||
"laravel",
|
||||
"search"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/scout/issues",
|
||||
"source": "https://github.com/laravel/scout"
|
||||
},
|
||||
"time": "2025-05-13T13:34:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/serializable-closure",
|
||||
"version": "v2.0.4",
|
||||
|
|
@ -2647,6 +2728,71 @@
|
|||
],
|
||||
"time": "2024-07-20T21:41:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "protonemedia/laravel-cross-eloquent-search",
|
||||
"version": "3.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/protonemedia/laravel-cross-eloquent-search.git",
|
||||
"reference": "8eb2a0b91e4c675abf434368bacc877c1bcd99ef"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/protonemedia/laravel-cross-eloquent-search/zipball/8eb2a0b91e4c675abf434368bacc877c1bcd99ef",
|
||||
"reference": "8eb2a0b91e4c675abf434368bacc877c1bcd99ef",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/support": "^10.48.28|^11.43|^12.0",
|
||||
"php": "^8.2|^8.3|^8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.4.4",
|
||||
"orchestra/testbench": "^8.0|^9.0|^10.0",
|
||||
"phpunit/phpunit": "^10.4|^11.5.3"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"ProtoneMedia\\LaravelCrossEloquentSearch\\ServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"ProtoneMedia\\LaravelCrossEloquentSearch\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Pascal Baljet",
|
||||
"email": "pascal@protone.media",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Laravel package to search through multiple Eloquent models. Supports pagination, eager loading relations, single/multiple columns, sorting and scoped queries.",
|
||||
"homepage": "https://github.com/protonemedia/laravel-cross-eloquent-search",
|
||||
"keywords": [
|
||||
"laravel-cross-eloquent-search",
|
||||
"protonemedia"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/protonemedia/laravel-cross-eloquent-search/issues",
|
||||
"source": "https://github.com/protonemedia/laravel-cross-eloquent-search/tree/3.6.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/pascalbaljet",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-02-19T13:08:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/clock",
|
||||
"version": "1.0.0",
|
||||
|
|
@ -3350,6 +3496,158 @@
|
|||
],
|
||||
"time": "2024-04-27T21:32:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-activitylog",
|
||||
"version": "4.10.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/laravel-activitylog.git",
|
||||
"reference": "466f30f7245fe3a6e328ad5e6812bd43b4bddea5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/466f30f7245fe3a6e328ad5e6812bd43b4bddea5",
|
||||
"reference": "466f30f7245fe3a6e328ad5e6812bd43b4bddea5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/config": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0",
|
||||
"illuminate/database": "^8.69 || ^9.27 || ^10.0 || ^11.0 || ^12.0",
|
||||
"illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0",
|
||||
"php": "^8.1",
|
||||
"spatie/laravel-package-tools": "^1.6.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-json": "*",
|
||||
"orchestra/testbench": "^6.23 || ^7.0 || ^8.0 || ^9.0 || ^10.0",
|
||||
"pestphp/pest": "^1.20 || ^2.0 || ^3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Spatie\\Activitylog\\ActivitylogServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Spatie\\Activitylog\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Freek Van der Herten",
|
||||
"email": "freek@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Sebastian De Deyne",
|
||||
"email": "sebastian@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Tom Witkowski",
|
||||
"email": "dev.gummibeer@gmail.com",
|
||||
"homepage": "https://gummibeer.de",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A very simple activity logger to monitor the users of your website or application",
|
||||
"homepage": "https://github.com/spatie/activitylog",
|
||||
"keywords": [
|
||||
"activity",
|
||||
"laravel",
|
||||
"log",
|
||||
"spatie",
|
||||
"user"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/laravel-activitylog/issues",
|
||||
"source": "https://github.com/spatie/laravel-activitylog/tree/4.10.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://spatie.be/open-source/support-us",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/spatie",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-02-10T15:38:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-package-tools",
|
||||
"version": "1.92.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/laravel-package-tools.git",
|
||||
"reference": "d20b1969f836d210459b78683d85c9cd5c5f508c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/d20b1969f836d210459b78683d85c9cd5c5f508c",
|
||||
"reference": "d20b1969f836d210459b78683d85c9cd5c5f508c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/contracts": "^9.28|^10.0|^11.0|^12.0",
|
||||
"php": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.5",
|
||||
"orchestra/testbench": "^7.7|^8.0|^9.0|^10.0",
|
||||
"pestphp/pest": "^1.23|^2.1|^3.1",
|
||||
"phpunit/php-code-coverage": "^9.0|^10.0|^11.0",
|
||||
"phpunit/phpunit": "^9.5.24|^10.5|^11.5",
|
||||
"spatie/pest-plugin-test-time": "^1.1|^2.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\LaravelPackageTools\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Freek Van der Herten",
|
||||
"email": "freek@spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Tools for creating Laravel packages",
|
||||
"homepage": "https://github.com/spatie/laravel-package-tools",
|
||||
"keywords": [
|
||||
"laravel-package-tools",
|
||||
"spatie"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/laravel-package-tools/issues",
|
||||
"source": "https://github.com/spatie/laravel-package-tools/tree/1.92.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/spatie",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-04-11T15:27:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/clock",
|
||||
"version": "v7.2.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
* If set to false, no activities will be saved to the database.
|
||||
*/
|
||||
'enabled' => env('ACTIVITY_LOGGER_ENABLED', true),
|
||||
|
||||
/*
|
||||
* When the clean-command is executed, all recording activities older than
|
||||
* the number of days specified here will be deleted.
|
||||
*/
|
||||
'delete_records_older_than_days' => 365,
|
||||
|
||||
/*
|
||||
* If no log name is passed to the activity() helper
|
||||
* we use this default log name.
|
||||
*/
|
||||
'default_log_name' => 'default',
|
||||
|
||||
/*
|
||||
* You can specify an auth driver here that gets user models.
|
||||
* If this is null we'll use the current Laravel auth driver.
|
||||
*/
|
||||
'default_auth_driver' => null,
|
||||
|
||||
/*
|
||||
* If set to true, the subject returns soft deleted models.
|
||||
*/
|
||||
'subject_returns_soft_deleted_models' => false,
|
||||
|
||||
/*
|
||||
* This model will be used to log activity.
|
||||
* It should implement the Spatie\Activitylog\Contracts\Activity interface
|
||||
* and extend Illuminate\Database\Eloquent\Model.
|
||||
*/
|
||||
'activity_model' => \Spatie\Activitylog\Models\Activity::class,
|
||||
|
||||
/*
|
||||
* This is the name of the table that will be created by the migration and
|
||||
* used by the Activity model shipped with this package.
|
||||
*/
|
||||
'table_name' => env('ACTIVITY_LOGGER_TABLE_NAME', 'activity_log'),
|
||||
|
||||
/*
|
||||
* This is the database connection that will be used by the migration and
|
||||
* the Activity model shipped with this package. In case it's not set
|
||||
* Laravel's database.default will be used instead.
|
||||
*/
|
||||
'database_connection' => env('ACTIVITY_LOGGER_DB_CONNECTION'),
|
||||
];
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Search Engine
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default search connection that gets used while
|
||||
| using Laravel Scout. This connection is used when syncing all models
|
||||
| to the search service. You should adjust this based on your needs.
|
||||
|
|
||||
| Supported: "algolia", "meilisearch", "typesense",
|
||||
| "database", "collection", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SCOUT_DRIVER', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Index Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify a prefix that will be applied to all search index
|
||||
| names used by Scout. This prefix may be useful if you have multiple
|
||||
| "tenants" or applications sharing the same search infrastructure.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('SCOUT_PREFIX', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Data Syncing
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to control if the operations that sync your data
|
||||
| with your search engines are queued. When this is set to "true" then
|
||||
| all automatic data syncing will get queued for better performance.
|
||||
|
|
||||
*/
|
||||
|
||||
'queue' => env('SCOUT_QUEUE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Transactions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This configuration option determines if your data will only be synced
|
||||
| with your search indexes after every open database transaction has
|
||||
| been committed, thus preventing any discarded data from syncing.
|
||||
|
|
||||
*/
|
||||
|
||||
'after_commit' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Chunk Sizes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options allow you to control the maximum chunk size when you are
|
||||
| mass importing data into the search engine. This allows you to fine
|
||||
| tune each of these chunk sizes based on the power of the servers.
|
||||
|
|
||||
*/
|
||||
|
||||
'chunk' => [
|
||||
'searchable' => 500,
|
||||
'unsearchable' => 500,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Soft Deletes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows to control whether to keep soft deleted records in
|
||||
| the search indexes. Maintaining soft deleted records can be useful
|
||||
| if your application still needs to search for the records later.
|
||||
|
|
||||
*/
|
||||
|
||||
'soft_delete' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Identify User
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to control whether to notify the search engine
|
||||
| of the user performing the search. This is sometimes useful if the
|
||||
| engine supports any analytics based on this application's users.
|
||||
|
|
||||
| Supported engines: "algolia"
|
||||
|
|
||||
*/
|
||||
|
||||
'identify' => env('SCOUT_IDENTIFY', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Algolia Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Algolia settings. Algolia is a cloud hosted
|
||||
| search engine which works great with Scout out of the box. Just plug
|
||||
| in your application ID and admin API key to get started searching.
|
||||
|
|
||||
*/
|
||||
|
||||
'algolia' => [
|
||||
'id' => env('ALGOLIA_APP_ID', ''),
|
||||
'secret' => env('ALGOLIA_SECRET', ''),
|
||||
'index-settings' => [
|
||||
// 'users' => [
|
||||
// 'searchableAttributes' => ['id', 'name', 'email'],
|
||||
// 'attributesForFaceting'=> ['filterOnly(email)'],
|
||||
// ],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Meilisearch Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Meilisearch settings. Meilisearch is an open
|
||||
| source search engine with minimal configuration. Below, you can state
|
||||
| the host and key information for your own Meilisearch installation.
|
||||
|
|
||||
| See: https://www.meilisearch.com/docs/learn/configuration/instance_options#all-instance-options
|
||||
|
|
||||
*/
|
||||
|
||||
'meilisearch' => [
|
||||
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
|
||||
'key' => env('MEILISEARCH_KEY'),
|
||||
'index-settings' => [
|
||||
// 'users' => [
|
||||
// 'filterableAttributes'=> ['id', 'name', 'email'],
|
||||
// ],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Typesense Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Typesense settings. Typesense is an open
|
||||
| source search engine using minimal configuration. Below, you will
|
||||
| state the host, key, and schema configuration for the instance.
|
||||
|
|
||||
*/
|
||||
|
||||
'typesense' => [
|
||||
'client-settings' => [
|
||||
'api_key' => env('TYPESENSE_API_KEY', 'xyz'),
|
||||
'nodes' => [
|
||||
[
|
||||
'host' => env('TYPESENSE_HOST', 'localhost'),
|
||||
'port' => env('TYPESENSE_PORT', '8108'),
|
||||
'path' => env('TYPESENSE_PATH', ''),
|
||||
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
|
||||
],
|
||||
],
|
||||
'nearest_node' => [
|
||||
'host' => env('TYPESENSE_HOST', 'localhost'),
|
||||
'port' => env('TYPESENSE_PORT', '8108'),
|
||||
'path' => env('TYPESENSE_PATH', ''),
|
||||
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
|
||||
],
|
||||
'connection_timeout_seconds' => env('TYPESENSE_CONNECTION_TIMEOUT_SECONDS', 2),
|
||||
'healthcheck_interval_seconds' => env('TYPESENSE_HEALTHCHECK_INTERVAL_SECONDS', 30),
|
||||
'num_retries' => env('TYPESENSE_NUM_RETRIES', 3),
|
||||
'retry_interval_seconds' => env('TYPESENSE_RETRY_INTERVAL_SECONDS', 1),
|
||||
],
|
||||
// 'max_total_results' => env('TYPESENSE_MAX_TOTAL_RESULTS', 1000),
|
||||
'model-settings' => [
|
||||
// User::class => [
|
||||
// 'collection-schema' => [
|
||||
// 'fields' => [
|
||||
// [
|
||||
// 'name' => 'id',
|
||||
// 'type' => 'string',
|
||||
// ],
|
||||
// [
|
||||
// 'name' => 'name',
|
||||
// 'type' => 'string',
|
||||
// ],
|
||||
// [
|
||||
// 'name' => 'created_at',
|
||||
// 'type' => 'int64',
|
||||
// ],
|
||||
// ],
|
||||
// 'default_sorting_field' => 'created_at',
|
||||
// ],
|
||||
// 'search-parameters' => [
|
||||
// 'query_by' => 'name'
|
||||
// ],
|
||||
// ],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?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()
|
||||
{
|
||||
Schema::create('activity_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null');
|
||||
$table->string('action'); // e.g., create, update, delete, duplicate
|
||||
$table->string('model_type'); // e.g., App\Models\Person
|
||||
$table->unsignedBigInteger('model_id');
|
||||
$table->json('changes')->nullable(); // Stores old and new values
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('activity_logs');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateActivityLogTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::connection(config('activitylog.database_connection'))->create(config('activitylog.table_name'), function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->string('log_name')->nullable();
|
||||
$table->text('description');
|
||||
$table->nullableMorphs('subject', 'subject');
|
||||
$table->nullableMorphs('causer', 'causer');
|
||||
$table->string('causer_name')->nullable(); // 👈 New column
|
||||
$table->json('properties')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index('log_name');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::connection(config('activitylog.database_connection'))->dropIfExists(config('activitylog.table_name'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddEventColumnToActivityLogTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
|
||||
$table->string('event')->nullable()->after('subject_type');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
|
||||
$table->dropColumn('event');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddBatchUuidColumnToActivityLogTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
|
||||
$table->uuid('batch_uuid')->nullable()->after('properties');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
|
||||
$table->dropColumn('batch_uuid');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ class DatabaseSeeder extends Seeder
|
|||
// Create admin user for testing the authentication system
|
||||
$this->call([
|
||||
AdminUserSeeder::class,
|
||||
PersonSeeder::class, // Seed 100 sample Person records
|
||||
]);
|
||||
|
||||
// Create a regular user for testing
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Person;
|
||||
use App\Models\Migration;
|
||||
use App\Models\Residence;
|
||||
use App\Models\Family;
|
||||
use App\Models\Naturalization;
|
||||
use App\Models\Internment;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Faker\Factory as Faker;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class PersonSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$faker = Faker::create();
|
||||
|
||||
$this->command->info('Creating 100 sample Person records with related data...');
|
||||
|
||||
// Create 100 sample Person records
|
||||
for ($i = 0; $i < 100; $i++) {
|
||||
$surname = $faker->lastName;
|
||||
$christianName = $faker->firstName;
|
||||
|
||||
// Randomly decide if person is deceased
|
||||
$isDeceased = $faker->boolean(30); // 30% chance of being deceased
|
||||
|
||||
// Generate dates (past dates for birth, future or null for death)
|
||||
$dob = $faker->dateTimeBetween('-100 years', '-20 years');
|
||||
$dod = $isDeceased ? $faker->dateTimeBetween($dob, 'now') : null;
|
||||
|
||||
// Create the person record
|
||||
$person = Person::create([
|
||||
'surname' => $surname,
|
||||
'christian_name' => $christianName,
|
||||
'full_name' => $christianName . ' ' . $surname,
|
||||
'date_of_birth' => $dob,
|
||||
'place_of_birth' => $faker->city . ', ' . $faker->country,
|
||||
'date_of_death' => $dod,
|
||||
'occupation' => $faker->jobTitle,
|
||||
'additional_notes' => $faker->boolean(70) ? $faker->paragraph(2) : null,
|
||||
'reference' => $faker->boolean(50) ? 'REF-' . $faker->randomNumber(5) : null,
|
||||
'id_card_no' => 'ID-' . $faker->unique()->randomNumber(8),
|
||||
]);
|
||||
|
||||
// Create Migration data (80% chance)
|
||||
if ($faker->boolean(80)) {
|
||||
$arrivalAus = $faker->dateTimeBetween($dob, '-1 years');
|
||||
$arrivalNT = $faker->dateTimeBetween($arrivalAus, '+2 years');
|
||||
|
||||
Migration::create([
|
||||
'person_id' => $person->person_id,
|
||||
'date_of_arrival_aus' => $arrivalAus,
|
||||
'date_of_arrival_nt' => $arrivalNT,
|
||||
'arrival_period' => $faker->randomElement(['Pre-WWII', 'Post-WWII', 'Modern Era']),
|
||||
'data_source' => $faker->randomElement(['Archives', 'Family Records', 'Historical Documents', 'Interviews']),
|
||||
]);
|
||||
}
|
||||
|
||||
// Create Residence data (70% chance)
|
||||
if ($faker->boolean(70)) {
|
||||
Residence::create([
|
||||
'person_id' => $person->person_id,
|
||||
'town_or_city' => $faker->city,
|
||||
'home_at_death' => $isDeceased ? $faker->streetAddress . ', ' . $faker->city : null,
|
||||
]);
|
||||
}
|
||||
|
||||
// Create Family data (60% chance)
|
||||
if ($faker->boolean(60)) {
|
||||
Family::create([
|
||||
'person_id' => $person->person_id,
|
||||
'names_of_parents' => $faker->boolean(70) ? $faker->name . ' & ' . $faker->name : null,
|
||||
'names_of_children' => $faker->boolean(50) ? implode(', ', $faker->words($faker->numberBetween(1, 4))) : null,
|
||||
]);
|
||||
}
|
||||
|
||||
// Create Naturalization data (40% chance)
|
||||
if ($faker->boolean(40)) {
|
||||
$naturalizationDate = $faker->dateTimeBetween($dob, 'now');
|
||||
|
||||
Naturalization::create([
|
||||
'person_id' => $person->person_id,
|
||||
'date_of_naturalisation' => $naturalizationDate,
|
||||
'no_of_cert' => 'CERT-' . $faker->unique()->randomNumber(6),
|
||||
'issued_at' => $faker->city,
|
||||
]);
|
||||
}
|
||||
|
||||
// Create Internment data (10% chance - rare historical event)
|
||||
if ($faker->boolean(10)) {
|
||||
|
||||
Internment::create([
|
||||
'person_id' => $person->person_id,
|
||||
'corps_issued' => $faker->randomElement(['Army', 'Navy', 'Air Force']),
|
||||
'interned_in' => $faker->city,
|
||||
'sent_to' => $faker->boolean(80) ? $faker->city : null,
|
||||
'internee_occupation' => $faker->jobTitle,
|
||||
'internee_address' => $faker->address,
|
||||
'cav' => $faker->boolean(50) ? $faker->randomNumber(5) : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info('100 sample Person records created successfully with related data');
|
||||
}
|
||||
}
|
||||
|
|
@ -3,31 +3,38 @@
|
|||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\PersonController;
|
||||
use App\Http\Controllers\PublicSearchController;
|
||||
use App\Http\Controllers\AuthController;
|
||||
use App\Http\Controllers\HistoricalSearchController;
|
||||
use App\Http\Controllers\DashboardController;
|
||||
use App\Http\Controllers\ActivityLogController;
|
||||
|
||||
Route::post('/login', [AuthController::class, 'login'])->name('login');
|
||||
Route::post('/register', [AuthController::class, 'register'])->name('register');
|
||||
|
||||
// Public search endpoints - allow searching without authentication
|
||||
Route::get('/persons/search', [PublicSearchController::class, 'search'])->name('persons.public.search');
|
||||
|
||||
// Public endpoint to get a specific migrant's full record
|
||||
Route::get('/migrants/{person_id}', [PublicSearchController::class, 'getRecord'])->name('migrants.get');
|
||||
Route::prefix('historical')->group(function () {
|
||||
Route::get('search', [HistoricalSearchController::class, 'search']);
|
||||
Route::get('record/{id}', [HistoricalSearchController::class, 'getRecord']);
|
||||
});
|
||||
|
||||
// Protected routes - require Sanctum authentication
|
||||
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::middleware('ability:admin')->group(function () {
|
||||
// Route::post('/register', [AuthController::class, 'register'])->name('register');
|
||||
// });
|
||||
|
||||
Route::get('/persons/search', [PersonController::class, 'search']);
|
||||
Route::get('/persons/{id}', [PersonController::class, 'show']);
|
||||
|
||||
Route::get('/activity-logs', [ActivityLogController::class, 'index']);
|
||||
// Person API endpoints - all CRUD operations protected by authentication
|
||||
Route::apiResource('persons', PersonController::class);
|
||||
|
||||
// Custom route for finding a person by ID card number
|
||||
Route::get('persons/id-card/{idCardNo}', [PersonController::class, 'findByIdCard'])->name('persons.findByIdCard');
|
||||
// Route::get('persons/id-card/{idCardNo}', [PersonController::class, 'findByIdCard'])->name('persons.findByIdCard');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AuthControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Test that anyone can register as a new user
|
||||
*/
|
||||
public function test_public_user_registration(): void
|
||||
{
|
||||
$userData = [
|
||||
'name' => 'New User',
|
||||
'email' => 'newuser@example.com',
|
||||
'password' => 'password123',
|
||||
];
|
||||
|
||||
$response = $this->postJson('/api/register', $userData);
|
||||
|
||||
$response->assertStatus(201)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => 'User created successfully',
|
||||
])
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'message',
|
||||
'data' => [
|
||||
'id',
|
||||
'name',
|
||||
'email',
|
||||
'updated_at',
|
||||
'created_at',
|
||||
]
|
||||
]);
|
||||
|
||||
// Verify the user was actually created in the database
|
||||
$this->assertDatabaseHas('users', [
|
||||
'name' => 'New User',
|
||||
'email' => 'newuser@example.com',
|
||||
]);
|
||||
|
||||
// Verify the user is not an admin by default
|
||||
$this->assertDatabaseHas('users', [
|
||||
'email' => 'newuser@example.com',
|
||||
'is_admin' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test validation rules for user registration
|
||||
*/
|
||||
public function test_registration_validation_rules(): void
|
||||
{
|
||||
// Create and authenticate an admin user
|
||||
$admin = User::factory()->create([
|
||||
'is_admin' => true
|
||||
]);
|
||||
|
||||
$this->actingAs($admin);
|
||||
|
||||
// Test empty fields
|
||||
$response = $this->postJson('/api/register', []);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['name', 'email', 'password']);
|
||||
|
||||
// Test email format validation
|
||||
$response = $this->postJson('/api/register', [
|
||||
'name' => 'New User',
|
||||
'email' => 'not-an-email',
|
||||
'password' => 'password123',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['email']);
|
||||
|
||||
// Test email uniqueness validation
|
||||
User::factory()->create(['email' => 'existing@example.com']);
|
||||
|
||||
$response = $this->postJson('/api/register', [
|
||||
'name' => 'New User',
|
||||
'email' => 'existing@example.com',
|
||||
'password' => 'password123',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['email']);
|
||||
|
||||
// Test password length validation
|
||||
$response = $this->postJson('/api/register', [
|
||||
'name' => 'New User',
|
||||
'email' => 'newuser@example.com',
|
||||
'password' => 'short',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['password']);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Person;
|
||||
use App\Models\Migration;
|
||||
use App\Models\Residence;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class HistoricalSearchControllerTest 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 historical search returns paginated results
|
||||
*/
|
||||
public function test_historical_search_returns_paginated_results(): void
|
||||
{
|
||||
// Create test data
|
||||
Person::factory(15)->create();
|
||||
|
||||
// Test without query parameter (should return all records paginated)
|
||||
$response = $this->getJson('/api/historical/search');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonStructure([
|
||||
'data',
|
||||
'current_page',
|
||||
'per_page',
|
||||
'last_page',
|
||||
'total',
|
||||
'links' => [
|
||||
'first',
|
||||
'last',
|
||||
'prev',
|
||||
'next'
|
||||
]
|
||||
])
|
||||
->assertJson([
|
||||
'per_page' => 10,
|
||||
'current_page' => 1
|
||||
]);
|
||||
|
||||
// Verify we got 10 items (pagination limit)
|
||||
$this->assertCount(10, $response->json('data'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test historical search with pagination parameters
|
||||
*/
|
||||
public function test_historical_search_pagination_works(): void
|
||||
{
|
||||
// Create test data - 25 records
|
||||
Person::factory(25)->create();
|
||||
|
||||
// Test first page
|
||||
$responsePage1 = $this->getJson('/api/historical/search?page=1');
|
||||
|
||||
$responsePage1->assertStatus(200)
|
||||
->assertJson([
|
||||
'current_page' => 1,
|
||||
'last_page' => 3 // 25 records with 10 per page = 3 pages
|
||||
]);
|
||||
|
||||
// Test second page
|
||||
$responsePage2 = $this->getJson('/api/historical/search?page=2');
|
||||
|
||||
$responsePage2->assertStatus(200)
|
||||
->assertJson([
|
||||
'current_page' => 2
|
||||
]);
|
||||
|
||||
// Verify different data between pages
|
||||
$this->assertNotEquals(
|
||||
$responsePage1->json('data')[0],
|
||||
$responsePage2->json('data')[0]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test historical search with query parameter
|
||||
*/
|
||||
public function test_historical_search_filters_by_query(): void
|
||||
{
|
||||
// Create test person with a unique name
|
||||
Person::factory()->create([
|
||||
'christian_name' => 'UniqueTestName',
|
||||
'surname' => 'TestSurname'
|
||||
]);
|
||||
|
||||
// Create other random records
|
||||
Person::factory(10)->create();
|
||||
|
||||
// Search for the unique name
|
||||
$response = $this->getJson('/api/historical/search?query=UniqueTestName');
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
// The search should find our unique record
|
||||
$this->assertStringContainsString('UniqueTestName', json_encode($response->json('data')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test search returns correct paginated links
|
||||
*/
|
||||
public function test_historical_search_returns_correct_links(): void
|
||||
{
|
||||
// Create enough data for multiple pages
|
||||
Person::factory(30)->create();
|
||||
|
||||
// Test middle page
|
||||
$response = $this->getJson('/api/historical/search?page=2');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonStructure([
|
||||
'links' => [
|
||||
'first',
|
||||
'last',
|
||||
'prev',
|
||||
'next'
|
||||
]
|
||||
]);
|
||||
|
||||
// Check links exist and contain page parameters
|
||||
$this->assertStringContainsString('page=1', $response->json('links.first'));
|
||||
$this->assertStringContainsString('page=3', $response->json('links.last'));
|
||||
$this->assertStringContainsString('page=1', $response->json('links.prev'));
|
||||
$this->assertStringContainsString('page=3', $response->json('links.next'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test search preserves query parameters in pagination links
|
||||
*/
|
||||
public function test_historical_search_preserves_query_params_in_pagination(): void
|
||||
{
|
||||
// Create 30 people with 'Test' in their name to ensure we have enough matching records
|
||||
Person::factory(30)->create([
|
||||
'christian_name' => 'Test Person'
|
||||
]);
|
||||
|
||||
// Search with a query and page=1 (so we definitely have a next page)
|
||||
$response = $this->getJson('/api/historical/search?query=Test&page=1');
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
// Verify links contain both the query and page parameters
|
||||
$this->assertStringContainsString('query=Test', $response->json('links.first'));
|
||||
$this->assertStringContainsString('query=Test', $response->json('links.last'));
|
||||
$this->assertStringContainsString('query=Test', $response->json('links.next'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test empty result handling
|
||||
*/
|
||||
public function test_historical_search_handles_empty_results(): void
|
||||
{
|
||||
// Search with a query that won't match anything
|
||||
$response = $this->getJson('/api/historical/search?query=NonExistentQuery123456');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'data' => [],
|
||||
'total' => 0,
|
||||
'last_page' => 1
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,469 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Models\User;
|
||||
use App\Models\Person;
|
||||
use App\Models\Migration;
|
||||
use App\Models\Residence;
|
||||
use App\Models\Naturalization;
|
||||
use App\Models\Family;
|
||||
use App\Models\Internment;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PublicSearchApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Set up test data before each test.
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Create test persons with related data for filtering tests
|
||||
$this->createTestData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test data for search tests.
|
||||
*/
|
||||
private function createTestData()
|
||||
{
|
||||
// Person 1: John Smith from Germany, arrived in 1880 at age 25, settled in Sydney
|
||||
$person1 = Person::create([
|
||||
'surname' => 'Smith',
|
||||
'christian_name' => 'John',
|
||||
'full_name' => 'John Smith',
|
||||
'date_of_birth' => '1855-03-15',
|
||||
'place_of_birth' => 'Berlin, Germany',
|
||||
'occupation' => 'Carpenter',
|
||||
'id_card_no' => 'TEST-001'
|
||||
]);
|
||||
|
||||
Migration::create([
|
||||
'person_id' => $person1->person_id,
|
||||
'date_of_arrival_aus' => '1880-06-10',
|
||||
'date_of_arrival_nt' => '1880-07-20',
|
||||
]);
|
||||
|
||||
Residence::create([
|
||||
'person_id' => $person1->person_id,
|
||||
'town_or_city' => 'Sydney',
|
||||
'home_at_death' => 'Sydney, NSW',
|
||||
]);
|
||||
|
||||
// Person 2: Maria Mueller from Austria, arrived in 1885 at age 22, settled in Melbourne
|
||||
$person2 = Person::create([
|
||||
'surname' => 'Mueller',
|
||||
'christian_name' => 'Maria',
|
||||
'full_name' => 'Maria Mueller',
|
||||
'date_of_birth' => '1863-09-28',
|
||||
'place_of_birth' => 'Vienna, Austria',
|
||||
'occupation' => 'Seamstress',
|
||||
'id_card_no' => 'TEST-002'
|
||||
]);
|
||||
|
||||
Migration::create([
|
||||
'person_id' => $person2->person_id,
|
||||
'date_of_arrival_aus' => '1885-04-15',
|
||||
'date_of_arrival_nt' => '1885-05-20',
|
||||
]);
|
||||
|
||||
Residence::create([
|
||||
'person_id' => $person2->person_id,
|
||||
'town_or_city' => 'Melbourne',
|
||||
'home_at_death' => 'Melbourne, VIC',
|
||||
]);
|
||||
|
||||
// Person 3: Robert Johnson from England, arrived in 1890 at age 30, settled in Brisbane
|
||||
$person3 = Person::create([
|
||||
'surname' => 'Johnson',
|
||||
'christian_name' => 'Robert',
|
||||
'full_name' => 'Robert Johnson',
|
||||
'date_of_birth' => '1860-05-10',
|
||||
'place_of_birth' => 'London, England',
|
||||
'occupation' => 'Teacher',
|
||||
'id_card_no' => 'TEST-003'
|
||||
]);
|
||||
|
||||
Migration::create([
|
||||
'person_id' => $person3->person_id,
|
||||
'date_of_arrival_aus' => '1890-08-12',
|
||||
'date_of_arrival_nt' => '1890-09-01',
|
||||
]);
|
||||
|
||||
Residence::create([
|
||||
'person_id' => $person3->person_id,
|
||||
'town_or_city' => 'Brisbane',
|
||||
'home_at_death' => 'Brisbane, QLD',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the endpoint returns 200 OK without authentication.
|
||||
*/
|
||||
public function test_public_search_endpoint_accessible_without_auth()
|
||||
{
|
||||
$response = $this->getJson('/api/persons/search');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'data' => [
|
||||
'data',
|
||||
'links',
|
||||
'meta',
|
||||
],
|
||||
'message'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that records are correctly filtered by firstName.
|
||||
*/
|
||||
public function test_filter_by_first_name()
|
||||
{
|
||||
$response = $this->getJson('/api/persons/search?firstName=John');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonPath('data.data.0.christian_name', 'John')
|
||||
->assertJsonCount(1, 'data.data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that records are correctly filtered by lastName.
|
||||
*/
|
||||
public function test_filter_by_last_name()
|
||||
{
|
||||
$response = $this->getJson('/api/persons/search?lastName=Mueller');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonPath('data.data.0.surname', 'Mueller')
|
||||
->assertJsonCount(1, 'data.data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that records are correctly filtered by regionOfOrigin.
|
||||
*/
|
||||
public function test_filter_by_region_of_origin()
|
||||
{
|
||||
$response = $this->getJson('/api/persons/search?regionOfOrigin=Germany');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonPath('data.data.0.place_of_birth', 'Berlin, Germany')
|
||||
->assertJsonCount(1, 'data.data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that records are correctly filtered by yearOfArrival.
|
||||
*/
|
||||
public function test_filter_by_year_of_arrival()
|
||||
{
|
||||
$response = $this->getJson('/api/persons/search?yearOfArrival=1885');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonCount(1, 'data.data')
|
||||
->assertJsonPath('data.data.0.surname', 'Mueller');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that records can be filtered by birth year rather than trying direct age calculation.
|
||||
* This is a simplification of the age at migration test to avoid SQL calculation issues.
|
||||
*/
|
||||
public function test_filter_by_birth_year_equivalent_to_age_at_migration()
|
||||
{
|
||||
// John Smith was born in 1855, which would make him 25 in 1880
|
||||
// Let's search for people born in the 1850s instead to simplify the test
|
||||
$response = $this->getJson('/api/persons/search?regionOfOrigin=Germany');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonCount(1, 'data.data')
|
||||
->assertJsonPath('data.data.0.surname', 'Smith');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that records are correctly filtered by settlementLocation.
|
||||
*/
|
||||
public function test_filter_by_settlement_location()
|
||||
{
|
||||
// First, verify we have all records without filters
|
||||
$response = $this->getJson('/api/persons/search');
|
||||
$response->assertStatus(200)
|
||||
->assertJsonCount(3, 'data.data');
|
||||
|
||||
// Now test filtering by settlement location using the new town_or_city field
|
||||
$response = $this->getJson('/api/persons/search?settlementLocation=Sydney');
|
||||
$response->assertStatus(200)
|
||||
->assertJsonCount(1, 'data.data')
|
||||
->assertJsonPath('data.data.0.surname', 'Smith');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the new town_or_city field stores and retrieves correctly through the API.
|
||||
*/
|
||||
public function test_town_or_city_field_works_correctly()
|
||||
{
|
||||
// Create a new person record with a residence
|
||||
$person = Person::create([
|
||||
'surname' => 'Darwin',
|
||||
'christian_name' => 'Charles',
|
||||
'full_name' => 'Charles Darwin',
|
||||
'date_of_birth' => '1809-02-12',
|
||||
'place_of_birth' => 'Shrewsbury, England',
|
||||
'occupation' => 'Naturalist',
|
||||
'id_card_no' => 'TEST-004'
|
||||
]);
|
||||
|
||||
// Create a residence record with the new town_or_city field
|
||||
$residence = Residence::create([
|
||||
'person_id' => $person->person_id,
|
||||
'town_or_city' => 'Darwin', // Using the town name as a test
|
||||
'home_at_death' => 'Down House, Kent'
|
||||
]);
|
||||
|
||||
// Retrieve the person through the API
|
||||
$response = $this->getJson("/api/migrants/{$person->person_id}");
|
||||
|
||||
// Assert the response contains the correct town_or_city value
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => 'Record retrieved successfully'
|
||||
])
|
||||
->assertJsonPath('data.residence.town_or_city', 'Darwin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that multiple filters can be combined.
|
||||
*/
|
||||
public function test_multiple_filters_combination()
|
||||
{
|
||||
$response = $this->getJson('/api/persons/search?firstName=John®ionOfOrigin=Germany');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonCount(1, 'data.data')
|
||||
->assertJsonPath('data.data.0.surname', 'Smith')
|
||||
->assertJsonPath('data.data.0.christian_name', 'John')
|
||||
->assertJsonPath('data.data.0.place_of_birth', 'Berlin, Germany');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that when no filters are applied, all records are returned.
|
||||
*/
|
||||
public function test_no_filters_returns_all_records()
|
||||
{
|
||||
$response = $this->getJson('/api/persons/search');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonCount(3, 'data.data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a migrant's full record can be retrieved by ID.
|
||||
*/
|
||||
public function test_get_migrant_by_id()
|
||||
{
|
||||
// Get a person ID from the test data
|
||||
$person = Person::where('id_card_no', 'TEST-001')->first();
|
||||
|
||||
// Call the endpoint
|
||||
$response = $this->getJson("/api/migrants/{$person->person_id}");
|
||||
|
||||
// Assert response structure and content
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => 'Record retrieved successfully'
|
||||
])
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'data' => [
|
||||
'person_id',
|
||||
'surname',
|
||||
'christian_name',
|
||||
'full_name',
|
||||
'date_of_birth',
|
||||
'place_of_birth',
|
||||
'occupation',
|
||||
'id_card_no',
|
||||
// Related models
|
||||
'migration',
|
||||
'naturalization',
|
||||
'residence',
|
||||
'family',
|
||||
'internment'
|
||||
],
|
||||
'message'
|
||||
]);
|
||||
|
||||
// Verify the correct record was returned
|
||||
$response->assertJsonPath('data.id_card_no', 'TEST-001');
|
||||
$response->assertJsonPath('data.christian_name', 'John');
|
||||
$response->assertJsonPath('data.surname', 'Smith');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that requesting a non-existent migrant ID returns 404.
|
||||
*/
|
||||
public function test_get_nonexistent_migrant_returns_404()
|
||||
{
|
||||
$response = $this->getJson('/api/migrants/999999');
|
||||
|
||||
$response->assertStatus(404);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that using "all" as a filter value results in no filtering for that field.
|
||||
*/
|
||||
public function test_all_value_means_no_filtering()
|
||||
{
|
||||
// Should return all 3 records because "all" means no filtering
|
||||
$response = $this->getJson('/api/persons/search?firstName=all&lastName=all');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonCount(3, 'data.data');
|
||||
|
||||
// Should return only Smith, even though lastName is set to "all"
|
||||
$response = $this->getJson('/api/persons/search?firstName=John&lastName=all');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonCount(1, 'data.data')
|
||||
->assertJsonPath('data.data.0.surname', 'Smith');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that POST requests to the search endpoint are not allowed.
|
||||
*/
|
||||
public function test_post_requests_not_allowed()
|
||||
{
|
||||
$response = $this->postJson('/api/persons/search');
|
||||
|
||||
$response->assertStatus(405); // Method Not Allowed
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that PUT requests to the search endpoint are not allowed.
|
||||
*/
|
||||
public function test_put_requests_not_allowed()
|
||||
{
|
||||
$response = $this->putJson('/api/persons/search');
|
||||
|
||||
// Laravel returns 401 for PUT because the route doesn't exist and it tries to authenticate
|
||||
$response->assertStatus(401)
|
||||
->assertJson([
|
||||
'message' => 'Unauthenticated.'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that PATCH requests to the search endpoint are not allowed.
|
||||
*/
|
||||
public function test_patch_requests_not_allowed()
|
||||
{
|
||||
$response = $this->patchJson('/api/persons/search');
|
||||
|
||||
// Laravel returns 401 for PATCH because the route doesn't exist and it tries to authenticate
|
||||
$response->assertStatus(401)
|
||||
->assertJson([
|
||||
'message' => 'Unauthenticated.'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that DELETE requests to the search endpoint are not allowed.
|
||||
*/
|
||||
public function test_delete_requests_not_allowed()
|
||||
{
|
||||
$response = $this->deleteJson('/api/persons/search');
|
||||
|
||||
// Laravel returns 401 for DELETE because the route doesn't exist and it tries to authenticate
|
||||
$response->assertStatus(401)
|
||||
->assertJson([
|
||||
'message' => 'Unauthenticated.'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test strict case-insensitive matching with at least one correct field returning results
|
||||
* despite other incorrect fields. This validates OR logic in search when a special parameter is used.
|
||||
*/
|
||||
public function test_one_correct_field_with_multiple_incorrect()
|
||||
{
|
||||
// Create a special test person with unique attributes for this test
|
||||
$specialPerson = Person::create([
|
||||
'surname' => 'UniqueLastName',
|
||||
'christian_name' => 'UniqueFirstName',
|
||||
'full_name' => 'UniqueFirstName UniqueLastName',
|
||||
'date_of_birth' => '1930-05-15',
|
||||
'place_of_birth' => 'UniqueRegion, UniqueCountry',
|
||||
'occupation' => 'Developer',
|
||||
'id_card_no' => 'UNIQUE-ID-123'
|
||||
]);
|
||||
|
||||
Migration::create([
|
||||
'person_id' => $specialPerson->person_id,
|
||||
'date_of_arrival_aus' => '1960-03-20',
|
||||
'date_of_arrival_nt' => '1960-04-10',
|
||||
]);
|
||||
|
||||
Residence::create([
|
||||
'person_id' => $specialPerson->person_id,
|
||||
'town_or_city' => 'UniqueCity',
|
||||
'home_at_death' => 'UniqueCity, UniqueState',
|
||||
]);
|
||||
|
||||
// Test case-insensitive matching for id_card_no
|
||||
$response = $this->getJson('/api/persons/search?id_card_no=unique-id-123');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonCount(1, 'data.data')
|
||||
->assertJsonPath('data.data.0.id_card_no', 'UNIQUE-ID-123');
|
||||
|
||||
// Test case-insensitive matching for firstName
|
||||
$response = $this->getJson('/api/persons/search?firstName=uniquefirstname');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonCount(1, 'data.data')
|
||||
->assertJsonPath('data.data.0.christian_name', 'UniqueFirstName');
|
||||
|
||||
// Test case-insensitive matching for lastName
|
||||
$response = $this->getJson('/api/persons/search?lastName=uniquelastname');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonCount(1, 'data.data')
|
||||
->assertJsonPath('data.data.0.surname', 'UniqueLastName');
|
||||
|
||||
// Test the OR logic where we provide one correct field and multiple incorrect ones
|
||||
// Using the useOrLogic parameter to apply OR condition instead of AND
|
||||
$response = $this->getJson('/api/persons/search?useOrLogic=true&id_card_no=unique-id-123&firstName=WrongName&lastName=WrongSurname');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonCount(1, 'data.data')
|
||||
->assertJsonPath('data.data.0.id_card_no', 'UNIQUE-ID-123');
|
||||
|
||||
// Another OR logic test with a different correct field
|
||||
$response = $this->getJson('/api/persons/search?useOrLogic=true&id_card_no=WRONG-ID&firstName=uniquefirstname®ionOfOrigin=WrongRegion');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonCount(1, 'data.data')
|
||||
->assertJsonPath('data.data.0.christian_name', 'UniqueFirstName');
|
||||
|
||||
// Test mixing capitalization in the correct field
|
||||
$response = $this->getJson('/api/persons/search?useOrLogic=true&id_card_no=WRONG-ID&firstName=WrongName&lastName=UniQueLastNAME');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonCount(1, 'data.data')
|
||||
->assertJsonPath('data.data.0.surname', 'UniqueLastName');
|
||||
|
||||
// Verify that normal behavior without useOrLogic returns no results for mixed inputs
|
||||
$response = $this->getJson('/api/persons/search?id_card_no=unique-id-123&firstName=WrongName');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonCount(0, 'data.data');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue