add CRUD Api client, client localisation, crud contact,

This commit is contained in:
Nyavokevin 2025-10-07 18:48:08 +03:00
parent 94eeb3e3d3
commit 215f4c4071
45 changed files with 1897 additions and 35 deletions

View File

@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreClientRequest;
use App\Http\Requests\UpdateClientRequest;
use App\Http\Resources\Client\ClientResource;
use App\Repositories\ClientRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
class ClientController extends Controller
{
public function __construct(
private readonly ClientRepositoryInterface $clientRepository
) {
}
/**
* Display a listing of clients.
*/
public function index(): AnonymousResourceCollection|JsonResponse
{
try {
$clients = $this->clientRepository->all();
return ClientResource::collection($clients);
} catch (\Exception $e) {
Log::error('Error fetching clients: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des clients.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created client.
*/
public function store(StoreClientRequest $request): ClientResource|JsonResponse
{
try {
$client = $this->clientRepository->create($request->validated());
return new ClientResource($client);
} catch (\Exception $e) {
Log::error('Error creating client: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création du client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified client.
*/
public function show(string $id): ClientResource|JsonResponse
{
try {
$client = $this->clientRepository->find($id);
if (!$client) {
return response()->json([
'message' => 'Client non trouvé.',
], 404);
}
return new ClientResource($client);
} catch (\Exception $e) {
Log::error('Error fetching client: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified client.
*/
public function update(UpdateClientRequest $request, string $id): ClientResource|JsonResponse
{
try {
$updated = $this->clientRepository->update($id, $request->validated());
if (!$updated) {
return response()->json([
'message' => 'Client non trouvé ou échec de la mise à jour.',
], 404);
}
$client = $this->clientRepository->find($id);
return new ClientResource($client);
} catch (\Exception $e) {
Log::error('Error updating client: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified client.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->clientRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Client non trouvé ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Client supprimé avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting client: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreClientGroupRequest;
use App\Http\Requests\UpdateClientGroupRequest;
use App\Http\Resources\Client\ClientGroupResource;
use App\Repositories\ClientGroupRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
class ClientGroupController extends Controller
{
public function __construct(
private readonly ClientGroupRepositoryInterface $clientGroupRepository
) {
}
/**
* Display a listing of client groups.
*/
public function index(): AnonymousResourceCollection|JsonResponse
{
try {
$clientGroups = $this->clientGroupRepository->all();
return ClientGroupResource::collection($clientGroups);
} catch (\Exception $e) {
Log::error('Error fetching client groups: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des groupes de clients.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created client group.
*/
public function store(StoreClientGroupRequest $request): ClientGroupResource|JsonResponse
{
try {
$clientGroup = $this->clientGroupRepository->create($request->validated());
return new ClientGroupResource($clientGroup);
} catch (\Exception $e) {
Log::error('Error creating client group: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création du groupe de clients.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified client group.
*/
public function show(string $id): ClientGroupResource|JsonResponse
{
try {
$clientGroup = $this->clientGroupRepository->find($id);
if (!$clientGroup) {
return response()->json([
'message' => 'Groupe de clients non trouvé.',
], 404);
}
return new ClientGroupResource($clientGroup);
} catch (\Exception $e) {
Log::error('Error fetching client group: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_group_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du groupe de clients.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified client group.
*/
public function update(UpdateClientGroupRequest $request, string $id): ClientGroupResource|JsonResponse
{
try {
$updated = $this->clientGroupRepository->update($id, $request->validated());
if (!$updated) {
return response()->json([
'message' => 'Groupe de clients non trouvé ou échec de la mise à jour.',
], 404);
}
$clientGroup = $this->clientGroupRepository->find($id);
return new ClientGroupResource($clientGroup);
} catch (\Exception $e) {
Log::error('Error updating client group: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_group_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du groupe de clients.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified client group.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->clientGroupRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Groupe de clients non trouvé ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Groupe de clients supprimé avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting client group: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_group_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du groupe de clients.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreClientLocationRequest;
use App\Http\Requests\UpdateClientLocationRequest;
use App\Http\Resources\Client\ClientLocationResource;
use App\Repositories\ClientLocationRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
class ClientLocationController extends Controller
{
public function __construct(
private readonly ClientLocationRepositoryInterface $clientLocationRepository
) {
}
/**
* Display a listing of client locations.
*/
public function index(): AnonymousResourceCollection|JsonResponse
{
try {
$clientLocations = $this->clientLocationRepository->all();
return ClientLocationResource::collection($clientLocations);
} catch (\Exception $e) {
Log::error('Error fetching client locations: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des lieux clients.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created client location.
*/
public function store(StoreClientLocationRequest $request): ClientLocationResource|JsonResponse
{
try {
$clientLocation = $this->clientLocationRepository->create($request->validated());
return new ClientLocationResource($clientLocation);
} catch (\Exception $e) {
Log::error('Error creating client location: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création du lieu client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified client location.
*/
public function show(string $id): ClientLocationResource|JsonResponse
{
try {
$clientLocation = $this->clientLocationRepository->find($id);
if (!$clientLocation) {
return response()->json([
'message' => 'Lieu client non trouvé.',
], 404);
}
return new ClientLocationResource($clientLocation);
} catch (\Exception $e) {
Log::error('Error fetching client location: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_location_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du lieu client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified client location.
*/
public function update(UpdateClientLocationRequest $request, string $id): ClientLocationResource|JsonResponse
{
try {
$updated = $this->clientLocationRepository->update($id, $request->validated());
if (!$updated) {
return response()->json([
'message' => 'Lieu client non trouvé ou échec de la mise à jour.',
], 404);
}
$clientLocation = $this->clientLocationRepository->find($id);
return new ClientLocationResource($clientLocation);
} catch (\Exception $e) {
Log::error('Error updating client location: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_location_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du lieu client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified client location.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->clientLocationRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Lieu client non trouvé ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Lieu client supprimé avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting client location: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_location_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du lieu client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreContactRequest;
use App\Http\Requests\UpdateContactRequest;
use App\Http\Resources\Contact\ContactResource;
use App\Repositories\ContactRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
class ContactController extends Controller
{
public function __construct(
private readonly ContactRepositoryInterface $contactRepository
) {
}
/**
* Display a listing of contacts.
*/
public function index(): AnonymousResourceCollection|JsonResponse
{
try {
$contacts = $this->contactRepository->all();
return ContactResource::collection($contacts);
} catch (\Exception $e) {
Log::error('Error fetching contacts: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des contacts.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created contact.
*/
public function store(StoreContactRequest $request): ContactResource|JsonResponse
{
try {
$contact = $this->contactRepository->create($request->validated());
return new ContactResource($contact);
} catch (\Exception $e) {
Log::error('Error creating contact: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création du contact.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified contact.
*/
public function show(string $id): ContactResource|JsonResponse
{
try {
$contact = $this->contactRepository->find($id);
if (!$contact) {
return response()->json([
'message' => 'Contact non trouvé.',
], 404);
}
return new ContactResource($contact);
} catch (\Exception $e) {
Log::error('Error fetching contact: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'contact_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du contact.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified contact.
*/
public function update(UpdateContactRequest $request, string $id): ContactResource|JsonResponse
{
try {
$updated = $this->contactRepository->update($id, $request->validated());
if (!$updated) {
return response()->json([
'message' => 'Contact non trouvé ou échec de la mise à jour.',
], 404);
}
$contact = $this->contactRepository->find($id);
return new ContactResource($contact);
} catch (\Exception $e) {
Log::error('Error updating contact: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'contact_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du contact.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified contact.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->contactRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Contact non trouvé ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Contact supprimé avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting contact: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'contact_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du contact.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreClientGroupRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:191|unique:client_groups,name',
'description' => 'nullable|string',
];
}
public function messages(): array
{
return [
'name.required' => 'Le nom du groupe est obligatoire.',
'name.string' => 'Le nom du groupe doit être une chaîne de caractères.',
'name.max' => 'Le nom du groupe ne peut pas dépasser 191 caractères.',
'name.unique' => 'Un groupe avec ce nom existe déjà.',
'description.string' => 'La description doit être une chaîne de caractères.',
];
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreClientLocationRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'client_id' => 'required|exists:clients,id',
'name' => 'nullable|string|max:191',
'address_line1' => 'nullable|string|max:255',
'address_line2' => 'nullable|string|max:255',
'postal_code' => 'nullable|string|max:20',
'city' => 'nullable|string|max:191',
'country_code' => 'nullable|string|size:2',
'gps_lat' => 'nullable|numeric|between:-90,90',
'gps_lng' => 'nullable|numeric|between:-180,180',
'is_default' => 'boolean',
];
}
public function messages(): array
{
return [
'client_id.required' => 'Le client est obligatoire.',
'client_id.exists' => 'Le client sélectionné n\'existe pas.',
'name.max' => 'Le nom ne peut pas dépasser 191 caractères.',
'address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.',
'address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.',
'postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.',
'city.max' => 'La ville ne peut pas dépasser 191 caractères.',
'country_code.size' => 'Le code pays doit contenir 2 caractères.',
'gps_lat.numeric' => 'La latitude doit être un nombre.',
'gps_lat.between' => 'La latitude doit être comprise entre -90 et 90.',
'gps_lng.numeric' => 'La longitude doit être un nombre.',
'gps_lng.between' => 'La longitude doit être comprise entre -180 et 180.',
'is_default.boolean' => 'Le statut par défaut doit être vrai ou faux.',
];
}
public function withValidator($validator)
{
$validator->after(function ($validator) {
if (empty($this->address_line1) && empty($this->postal_code) && empty($this->city)) {
$validator->errors()->add(
'general',
'Au moins un champ d\'adresse (adresse, code postal ou ville) doit être renseigné.'
);
}
});
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreClientRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'type' => 'required|in:pompes_funebres,famille,entreprise,collectivite,autre',
'name' => 'required|string|max:255',
'vat_number' => 'nullable|string|max:32',
'siret' => 'nullable|string|max:20',
'email' => 'nullable|email|max:191',
'phone' => 'nullable|string|max:50',
'billing_address_line1' => 'nullable|string|max:255',
'billing_address_line2' => 'nullable|string|max:255',
'billing_postal_code' => 'nullable|string|max:20',
'billing_city' => 'nullable|string|max:191',
'billing_country_code' => 'nullable|string|size:2',
'group_id' => 'nullable|exists:client_groups,id',
'notes' => 'nullable|string',
'is_active' => 'boolean',
'default_tva_rate_id' => 'nullable|exists:tva_rates,id',
];
}
public function messages(): array
{
return [
'company_id.required' => 'La société est obligatoire.',
'company_id.exists' => 'La société sélectionnée n\'existe pas.',
'type.required' => 'Le type de client est obligatoire.',
'type.in' => 'Le type de client sélectionné est invalide.',
'name.required' => 'Le nom du client est obligatoire.',
'name.string' => 'Le nom du client doit être une chaîne de caractères.',
'name.max' => 'Le nom du client ne peut pas dépasser 255 caractères.',
'vat_number.max' => 'Le numéro de TVA ne peut pas dépasser 32 caractères.',
'siret.max' => 'Le SIRET ne peut pas dépasser 20 caractères.',
'email.email' => 'L\'adresse email doit être valide.',
'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.',
'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.',
'billing_address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.',
'billing_address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.',
'billing_postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.',
'billing_city.max' => 'La ville ne peut pas dépasser 191 caractères.',
'billing_country_code.size' => 'Le code pays doit contenir 2 caractères.',
'group_id.exists' => 'Le groupe de clients sélectionné n\'existe pas.',
'is_active.boolean' => 'Le statut actif doit être vrai ou faux.',
'default_tva_rate_id.exists' => 'Le taux de TVA sélectionné n\'existe pas.',
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreContactRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateClientGroupRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$clientGroupId = $this->route('client_group') ? $this->route('client_group')->id : $this->route('id');
return [
'name' => [
'required',
'string',
'max:191',
Rule::unique('client_groups', 'name')->ignore($clientGroupId)
],
'description' => 'nullable|string',
];
}
public function messages(): array
{
return [
'name.required' => 'Le nom du groupe est obligatoire.',
'name.string' => 'Le nom du groupe doit être une chaîne de caractères.',
'name.max' => 'Le nom du groupe ne peut pas dépasser 191 caractères.',
'name.unique' => 'Un groupe avec ce nom existe déjà.',
'description.string' => 'La description doit être une chaîne de caractères.',
];
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateClientLocationRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'client_id' => 'required|exists:clients,id',
'name' => 'nullable|string|max:191',
'address_line1' => 'nullable|string|max:255',
'address_line2' => 'nullable|string|max:255',
'postal_code' => 'nullable|string|max:20',
'city' => 'nullable|string|max:191',
'country_code' => 'nullable|string|size:2',
'gps_lat' => 'nullable|numeric|between:-90,90',
'gps_lng' => 'nullable|numeric|between:-180,180',
'is_default' => 'boolean',
];
}
public function messages(): array
{
return [
'client_id.required' => 'Le client est obligatoire.',
'client_id.exists' => 'Le client sélectionné n\'existe pas.',
'name.max' => 'Le nom ne peut pas dépasser 191 caractères.',
'address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.',
'address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.',
'postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.',
'city.max' => 'La ville ne peut pas dépasser 191 caractères.',
'country_code.size' => 'Le code pays doit contenir 2 caractères.',
'gps_lat.numeric' => 'La latitude doit être un nombre.',
'gps_lat.between' => 'La latitude doit être comprise entre -90 et 90.',
'gps_lng.numeric' => 'La longitude doit être un nombre.',
'gps_lng.between' => 'La longitude doit être comprise entre -180 et 180.',
'is_default.boolean' => 'Le statut par défaut doit être vrai ou faux.',
];
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateClientRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'type' => 'required|in:pompes_funebres,famille,entreprise,collectivite,autre',
'name' => 'required|string|max:255',
'vat_number' => 'nullable|string|max:32',
'siret' => 'nullable|string|max:20',
'email' => 'nullable|email|max:191',
'phone' => 'nullable|string|max:50',
'billing_address_line1' => 'nullable|string|max:255',
'billing_address_line2' => 'nullable|string|max:255',
'billing_postal_code' => 'nullable|string|max:20',
'billing_city' => 'nullable|string|max:191',
'billing_country_code' => 'nullable|string|size:2',
'group_id' => 'nullable|exists:client_groups,id',
'notes' => 'nullable|string',
'is_active' => 'boolean',
'default_tva_rate_id' => 'nullable|exists:tva_rates,id',
];
}
public function messages(): array
{
return [
'company_id.required' => 'La société est obligatoire.',
'company_id.exists' => 'La société sélectionnée n\'existe pas.',
'type.required' => 'Le type de client est obligatoire.',
'type.in' => 'Le type de client sélectionné est invalide.',
'name.required' => 'Le nom du client est obligatoire.',
'name.string' => 'Le nom du client doit être une chaîne de caractères.',
'name.max' => 'Le nom du client ne peut pas dépasser 255 caractères.',
'vat_number.max' => 'Le numéro de TVA ne peut pas dépasser 32 caractères.',
'siret.max' => 'Le SIRET ne peut pas dépasser 20 caractères.',
'email.email' => 'L\'adresse email doit être valide.',
'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.',
'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.',
'billing_address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.',
'billing_address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.',
'billing_postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.',
'billing_city.max' => 'La ville ne peut pas dépasser 191 caractères.',
'billing_country_code.size' => 'Le code pays doit contenir 2 caractères.',
'group_id.exists' => 'Le groupe de clients sélectionné n\'existe pas.',
'is_active.boolean' => 'Le statut actif doit être vrai ou faux.',
'default_tva_rate_id.exists' => 'Le taux de TVA sélectionné n\'existe pas.',
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateContactRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ClientCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
'from' => $this->firstItem(),
'to' => $this->lastItem(),
'stats' => [
'active' => $this->collection->where('is_active', true)->count(),
'inactive' => $this->collection->where('is_active', false)->count(),
'by_type' => $this->collection->groupBy('type')->map->count(),
],
],
'links' => [
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
];
}
public function with(Request $request): array
{
return [
'status' => 'success',
'message' => 'Clients récupérés avec succès',
];
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ClientGroupCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
'from' => $this->firstItem(),
'to' => $this->lastItem(),
],
'links' => [
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
];
}
public function with(Request $request): array
{
return [
'status' => 'success',
'message' => 'Groupes de clients récupérés avec succès',
];
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Resources\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ClientGroupResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description ?? null,
'created_at' => $this->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'),
];
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Http\Resources\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ClientLocationCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
'from' => $this->firstItem(),
'to' => $this->lastItem(),
'stats' => [
'default_locations' => $this->collection->where('is_default', true)->count(),
'with_gps' => $this->collection->filter(fn($location) => $location->gps_lat && $location->gps_lng)->count(),
],
],
'links' => [
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
];
}
public function with(Request $request): array
{
return [
'status' => 'success',
'message' => 'Lieux clients récupérés avec succès',
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ClientLocationResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Http\Resources\Client;
use App\Http\Resources\Contact\ContactResource;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ClientResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
//'company_id' => $this->company_id,
'type' => $this->type,
'type_label' => $this->getTypeLabel(),
'name' => $this->name,
'vat_number' => $this->vat_number,
'siret' => $this->siret,
'email' => $this->email,
'phone' => $this->phone,
'billing_address' => [
'line1' => $this->billing_address_line1,
'line2' => $this->billing_address_line2,
'postal_code' => $this->billing_postal_code,
'city' => $this->billing_city,
'country_code' => $this->billing_country_code,
'full_address' => $this->billing_address,
],
'group_id' => $this->group_id,
'notes' => $this->notes,
'is_active' => $this->is_active,
// 'default_tva_rate_id' => $this->default_tva_rate_id,
'created_at' => $this->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'),
// Counts
'contacts_count' => $this->whenCounted('contacts'),
'locations_count' => $this->whenCounted('locations'),
// 'interventions_count' => $this->whenCounted('interventions'),
// 'quotes_count' => $this->whenCounted('quotes'),
// 'invoices_count' => $this->whenCounted('invoices'),
// Relations
// 'company' => new CompanyResource($this->whenLoaded('company')),
'group' => new ClientGroupResource($this->whenLoaded('group')),
// 'default_tva_rate' => new TvaRateResource($this->whenLoaded('defaultTvaRate')),
'contacts' => ContactResource::collection($this->whenLoaded('contacts')),
'locations' => ClientLocationResource::collection($this->whenLoaded('locations')),
// 'price_lists' => PriceListResource::collection($this->whenLoaded('priceLists')),
// 'interventions' => InterventionResource::collection($this->whenLoaded('interventions')),
// 'quotes' => QuoteResource::collection($this->whenLoaded('quotes')),
// 'invoices' => InvoiceResource::collection($this->whenLoaded('invoices')),
];
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Resources\Contact;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ContactCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
'from' => $this->firstItem(),
'to' => $this->lastItem(),
],
'links' => [
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
];
}
public function with(Request $request): array
{
return [
'status' => 'success',
'message' => 'Contacts récupérés avec succès',
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources\Contact;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ContactResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Client extends Model
{
protected $fillable = [
'type',
'name',
'vat_number',
'siret',
'email',
'phone',
'billing_address_line1',
'billing_address_line2',
'billing_postal_code',
'billing_city',
'billing_country_code',
'group_id',
'notes',
'is_active',
'default_tva_rate_id',
];
protected $casts = [
'is_active' => 'boolean',
];
/**
* Get the human-readable label for the client type.
*/
public function getTypeLabel(): string
{
return match($this->type) {
'pompes_funebres' => 'Pompes funèbres',
'famille' => 'Famille',
'entreprise' => 'Entreprise',
'collectivite' => 'Collectivité',
'autre' => 'Autre',
default => $this->type,
};
}
/**
* Get the full billing address as a string.
*/
public function getBillingAddressAttribute(): ?string
{
$parts = array_filter([
$this->billing_address_line1,
$this->billing_address_line2,
$this->billing_postal_code ? $this->billing_postal_code . ' ' . $this->billing_city : $this->billing_city,
$this->billing_country_code,
]);
return !empty($parts) ? implode(', ', $parts) : null;
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ClientContact extends Model
{
//
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ClientGroup extends Model
{
protected $fillable = [
'name',
'description',
];
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ClientLocation extends Model
{
protected $fillable = [
'client_id',
'name',
'address_line1',
'address_line2',
'postal_code',
'city',
'country_code',
'gps_lat',
'gps_lng',
'is_default',
];
protected $casts = [
'is_default' => 'boolean',
'gps_lat' => 'decimal:8',
'gps_lng' => 'decimal:8',
];
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Contact extends Model
{
protected $fillable = [
'first_name',
'last_name',
'email',
'phone',
'mobile',
'position',
'notes',
'is_primary',
];
protected $casts = [
'is_primary' => 'boolean',
];
}

View File

@ -11,7 +11,26 @@ class AppServiceProvider extends ServiceProvider
*/
public function register(): void
{
//
// Repository interface to implementation bindings
$this->app->bind(\App\Repositories\ClientRepositoryInterface::class, function ($app) {
return new \App\Repositories\ClientRepository($app->make(\App\Models\Client::class));
});
$this->app->bind(\App\Repositories\ClientGroupRepositoryInterface::class, function ($app) {
return new \App\Repositories\ClientGroupRepository($app->make(\App\Models\ClientGroup::class));
});
$this->app->bind(\App\Repositories\ClientContactRepositoryInterface::class, function ($app) {
return new \App\Repositories\ClientContactRepository($app->make(\App\Models\ClientContact::class));
});
$this->app->bind(\App\Repositories\ContactRepositoryInterface::class, function ($app) {
return new \App\Repositories\ContactRepository($app->make(\App\Models\Contact::class));
});
$this->app->bind(\App\Repositories\ClientLocationRepositoryInterface::class, function ($app) {
return new \App\Repositories\ClientLocationRepository($app->make(\App\Models\ClientLocation::class));
});
}
/**

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* Base repository implementation using Eloquent models.
*/
class BaseRepository implements BaseRepositoryInterface
{
public function __construct(protected Model $model)
{
}
/**
* @param array<int, string> $columns
* @return Collection<int, Model>
*/
public function all(array $columns = ['*']): Collection
{
return $this->model->newQuery()->get($columns);
}
/**
* @param int|string $id
* @param array<int, string> $columns
*/
public function find(int|string $id, array $columns = ['*']): ?Model
{
return $this->model->newQuery()->find($id, $columns);
}
/**
* @param array<string, mixed> $attributes
*/
public function create(array $attributes): Model
{
// Uses mass assignment; ensure $fillable is set on the model
return $this->model->newQuery()->create($attributes);
}
/**
* @param int|string $id
* @param array<string, mixed> $attributes
*/
public function update(int|string $id, array $attributes): bool
{
$instance = $this->find($id);
if (! $instance) {
return false;
}
return $instance->fill($attributes)->save();
}
/**
* @param int|string $id
*/
public function delete(int|string $id): bool
{
$instance = $this->find($id);
if (! $instance) {
return false;
}
return (bool) $instance->delete();
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* Base repository contract for simple CRUD operations using Eloquent models.
*/
interface BaseRepositoryInterface
{
/**
* Get all records.
*
* @param array<int, string> $columns
* @return Collection<int, Model>
*/
public function all(array $columns = ['*']): Collection;
/**
* Find a record by its primary key.
*
* @param int|string $id
* @param array<int, string> $columns
*/
public function find(int|string $id, array $columns = ['*']): ?Model;
/**
* Create a new record with the given attributes.
*
* @param array<string, mixed> $attributes
*/
public function create(array $attributes): Model;
/**
* Update an existing record by id with the given attributes.
*
* @param int|string $id
* @param array<string, mixed> $attributes
*/
public function update(int|string $id, array $attributes): bool;
/**
* Delete a record by its primary key.
*
* @param int|string $id
*/
public function delete(int|string $id): bool;
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\ClientContact;
class ClientContactRepository extends BaseRepository implements ClientContactRepositoryInterface
{
public function __construct(ClientContact $model)
{
parent::__construct($model);
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
interface ClientContactRepositoryInterface extends BaseRepositoryInterface
{
// Add ClientContact-specific methods here later if needed
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\ClientGroup;
class ClientGroupRepository extends BaseRepository implements ClientGroupRepositoryInterface
{
public function __construct(ClientGroup $model)
{
parent::__construct($model);
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
interface ClientGroupRepositoryInterface extends BaseRepositoryInterface
{
// Add ClientGroup-specific methods here later if needed
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\ClientLocation;
class ClientLocationRepository extends BaseRepository implements ClientLocationRepositoryInterface
{
public function __construct(ClientLocation $model)
{
parent::__construct($model);
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
interface ClientLocationRepositoryInterface extends BaseRepositoryInterface
{
// Add ClientLocation-specific methods here later if needed
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\Client;
class ClientRepository extends BaseRepository implements ClientRepositoryInterface
{
public function __construct(Client $model)
{
parent::__construct($model);
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
interface ClientRepositoryInterface extends BaseRepositoryInterface
{
// Add Client-specific methods here later if needed
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\Contact;
class ContactRepository extends BaseRepository implements ContactRepositoryInterface
{
public function __construct(Contact $model)
{
parent::__construct($model);
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
interface ContactRepositoryInterface extends BaseRepositoryInterface
{
// Add Contact-specific methods here later if needed
}

View File

@ -12,7 +12,7 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
$middleware->statefulApi();
})
->withExceptions(function (Exceptions $exceptions): void {
//

View File

@ -1,33 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->text('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};

View File

@ -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(): void
{
Schema::create('client_groups', function (Blueprint $table) {
$table->id();
$table->string('name', 191)->unique();
$table->text('description')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('client_groups');
}
};

View File

@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('clients', function (Blueprint $table) {
$table->id();
$table->enum('type', [
'pompes_funebres',
'famille',
'entreprise',
'collectivite',
'autre'
])->default('pompes_funebres');
$table->string('name', 255);
$table->string('vat_number', 32)->nullable();
$table->string('siret', 20)->nullable();
$table->string('email', 191)->nullable();
$table->string('phone', 50)->nullable();
$table->string('billing_address_line1', 255)->nullable();
$table->string('billing_address_line2', 255)->nullable();
$table->string('billing_postal_code', 20)->nullable();
$table->string('billing_city', 191)->nullable();
$table->char('billing_country_code', 2)->default('FR');
$table->foreignId('group_id')->nullable()->constrained('client_groups')->onDelete('set null');
$table->text('notes')->nullable();
$table->boolean('is_active')->default(true);
//$table->foreignId('default_tva_rate_id')->nullable()->constrained('tva_rates')->onDelete('set null');
$table->timestamps();
$table->index(['group_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('clients');
}
};

View File

@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('contacts', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('contacts');
}
};

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('client_locations', function (Blueprint $table) {
$table->id();
$table->foreignId('client_id')->constrained()->onDelete('cascade');
$table->string('name', 191)->nullable();
$table->string('address_line1', 255)->nullable();
$table->string('address_line2', 255)->nullable();
$table->string('postal_code', 20)->nullable();
$table->string('city', 191)->nullable();
$table->char('country_code', 2)->default('FR');
$table->double('gps_lat')->nullable();
$table->double('gps_lng')->nullable();
$table->boolean('is_default')->default(false);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('client_locations');
}
};

View File

@ -2,6 +2,10 @@
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\ClientController;
use App\Http\Controllers\Api\ClientGroupController;
use App\Http\Controllers\Api\ClientLocationController;
use App\Http\Controllers\Api\ContactController;
/*
|--------------------------------------------------------------------------
@ -25,3 +29,14 @@ Route::prefix('auth')->group(function () {
Route::post('/logout-all', [AuthController::class, 'logoutAll']);
});
});
// Protected API routes
Route::middleware('auth:sanctum')->group(function () {
// Client management
Route::apiResource('clients', ClientController::class);
Route::apiResource('client-groups', ClientGroupController::class);
Route::apiResource('client-locations', ClientLocationController::class);
// Contact management
Route::apiResource('contacts', ContactController::class);
});