From 56b0c50111ca270b4b18752ae55460c47ce916ae Mon Sep 17 00:00:00 2001 From: nyavokevin Date: Wed, 8 Apr 2026 13:31:57 +0300 Subject: [PATCH] feat(auth): add employee user linking and password setup flow Add user management endpoints and link employees to existing users through `user_id`, including API resources, validation, repository support, and database migrations. Introduce a two-step login flow that checks email first and lets users without a password create one before signing in. Update the employee detail UI with a dedicated user tab and refresh the employee and intervention side navigation to support the new account management flow. --- .../Http/Controllers/Api/AuthController.php | 69 +++ .../Http/Controllers/Api/UserController.php | 173 +++++++ .../app/Http/Requests/StoreUserRequest.php | 28 + .../Http/Requests/UpdateEmployeeRequest.php | 6 +- .../app/Http/Requests/UpdateUserRequest.php | 35 ++ .../Resources/Employee/EmployeeResource.php | 10 + thanasoft-back/app/Models/Employee.php | 7 + thanasoft-back/app/Models/User.php | 6 + .../app/Providers/AppServiceProvider.php | 4 + .../app/Repositories/EmployeeRepository.php | 13 +- .../app/Repositories/UserRepository.php | 15 + .../Repositories/UserRepositoryInterface.php | 9 + ..._111700_add_user_id_to_employees_table.php | 33 ++ ...08_113400_make_users_password_nullable.php | 28 + thanasoft-back/routes/api.php | 4 + .../CRM/EmployeeDetailPresentation.vue | 239 +++++++-- .../CRM/employee/EmployeeDetailContent.vue | 138 ++++- .../CRM/employee/EmployeeDetailSidebar.vue | 366 +++++++++++-- .../InterventionDetailPresentation.vue | 189 ++----- .../molecules/employee/EmployeeOverview.vue | 488 ++++++++---------- .../employee/EmployeeProfileCard.vue | 247 ++++++--- .../employee/EmployeeTabNavigation.vue | 129 +++-- .../molecules/employee/EmployeeUserTab.vue | 275 ++++++++++ .../InterventionTabNavigation.vue | 43 +- thanasoft-front/src/services/auth.ts | 43 ++ thanasoft-front/src/services/employee.ts | 7 + thanasoft-front/src/services/user.ts | 65 +++ thanasoft-front/src/stores/auth.ts | 19 +- thanasoft-front/src/stores/userStore.ts | 79 +++ .../src/views/pages/CRM/EmployeeDetails.vue | 12 +- thanasoft-front/src/views/pages/Login.vue | 207 ++++++-- thanasoft-front/tsconfig.tsbuildinfo | 49 +- 32 files changed, 2334 insertions(+), 701 deletions(-) create mode 100644 thanasoft-back/app/Http/Controllers/Api/UserController.php create mode 100644 thanasoft-back/app/Http/Requests/StoreUserRequest.php create mode 100644 thanasoft-back/app/Http/Requests/UpdateUserRequest.php create mode 100644 thanasoft-back/app/Repositories/UserRepository.php create mode 100644 thanasoft-back/app/Repositories/UserRepositoryInterface.php create mode 100644 thanasoft-back/database/migrations/2026_04_08_111700_add_user_id_to_employees_table.php create mode 100644 thanasoft-back/database/migrations/2026_04_08_113400_make_users_password_nullable.php create mode 100644 thanasoft-front/src/components/molecules/employee/EmployeeUserTab.vue create mode 100644 thanasoft-front/src/services/user.ts create mode 100644 thanasoft-front/src/stores/userStore.ts diff --git a/thanasoft-back/app/Http/Controllers/Api/AuthController.php b/thanasoft-back/app/Http/Controllers/Api/AuthController.php index 9fd413b..aa01f6f 100644 --- a/thanasoft-back/app/Http/Controllers/Api/AuthController.php +++ b/thanasoft-back/app/Http/Controllers/Api/AuthController.php @@ -72,6 +72,75 @@ class AuthController extends BaseController } } + public function checkEmail(Request $request): JsonResponse + { + try { + $data = $request->validate([ + 'email' => ['required', 'email'], + ]); + + $user = User::where('email', $data['email'])->first(); + + if (! $user) { + return $this->sendError('Utilisateur introuvable.', [ + 'email' => ['Aucun utilisateur ne correspond a cet email.'], + ], 404); + } + + return $this->sendResponse([ + 'user' => [ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + ], + 'has_password' => ! empty($user->getRawOriginal('password')), + ], 'Email verifie avec succes.'); + } catch (ValidationException $e) { + return $this->sendError('Validation Error.', $e->errors(), 422); + } catch (\Exception $e) { + return $this->sendError('Email check failed.', ['error' => $e->getMessage()], 500); + } + } + + public function createPasswordAndLogin(Request $request): JsonResponse + { + try { + $data = $request->validate([ + 'email' => ['required', 'email'], + 'password' => ['required', 'confirmed', Password::min(8)], + ]); + + /** @var User|null $user */ + $user = User::where('email', $data['email'])->first(); + + if (! $user) { + return $this->sendError('Utilisateur introuvable.', [ + 'email' => ['Aucun utilisateur ne correspond a cet email.'], + ], 404); + } + + if (! empty($user->getRawOriginal('password'))) { + return $this->sendError('Mot de passe deja defini.', [ + 'password' => ['Cet utilisateur a deja un mot de passe.'], + ], 422); + } + + $user->password = $data['password']; + $user->save(); + + $token = $user->createToken('api')->plainTextToken; + + return $this->sendResponse([ + 'user' => $user, + 'token' => $token, + ], 'Mot de passe cree et connexion reussie.'); + } catch (ValidationException $e) { + return $this->sendError('Validation Error.', $e->errors(), 422); + } catch (\Exception $e) { + return $this->sendError('Password creation failed.', ['error' => $e->getMessage()], 500); + } + } + public function me(Request $request): JsonResponse { try { diff --git a/thanasoft-back/app/Http/Controllers/Api/UserController.php b/thanasoft-back/app/Http/Controllers/Api/UserController.php new file mode 100644 index 0000000..ae3318f --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/UserController.php @@ -0,0 +1,173 @@ +query('email'); + + if ($email) { + $user = User::query()->where('email', $email)->first(); + + return response()->json([ + 'data' => $user, + 'message' => $user + ? 'Utilisateur recupere avec succes.' + : 'Aucun utilisateur trouve pour cet email.', + ]); + } + + return response()->json([ + 'data' => $this->userRepository->all()->sortBy('name')->values(), + 'message' => 'Utilisateurs recuperes avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error fetching users: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recuperation des utilisateurs.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function store(StoreUserRequest $request): JsonResponse + { + try { + $user = $this->userRepository->create($request->validated()); + + return response()->json([ + 'data' => $user, + 'message' => 'Utilisateur cree avec succes.', + ], 201); + } catch (\Exception $e) { + Log::error('Error creating user: ' . $e->getMessage(), [ + 'exception' => $e, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la creation de l\'utilisateur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function show(string $id): JsonResponse + { + try { + $user = $this->userRepository->find($id); + + if (! $user) { + return response()->json([ + 'message' => 'Utilisateur non trouve.', + ], 404); + } + + return response()->json([ + 'data' => $user, + 'message' => 'Utilisateur recupere avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error fetching user: ' . $e->getMessage(), [ + 'exception' => $e, + 'user_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recuperation de l\'utilisateur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function update(UpdateUserRequest $request, string $id): JsonResponse + { + try { + $user = $this->userRepository->find($id); + + if (! $user) { + return response()->json([ + 'message' => 'Utilisateur non trouve ou echec de la mise a jour.', + ], 404); + } + + $validated = $request->validated(); + + if (empty($validated['password'])) { + unset($validated['password']); + } + + $updated = $user->fill($validated)->save(); + + if (! $updated) { + return response()->json([ + 'message' => 'Utilisateur non trouve ou echec de la mise a jour.', + ], 404); + } + + return response()->json([ + 'data' => $user->fresh(), + 'message' => 'Utilisateur mis a jour avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error updating user: ' . $e->getMessage(), [ + 'exception' => $e, + 'user_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise a jour de l\'utilisateur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->userRepository->delete($id); + + if (! $deleted) { + return response()->json([ + 'message' => 'Utilisateur non trouve ou echec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Utilisateur supprime avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error deleting user: ' . $e->getMessage(), [ + 'exception' => $e, + 'user_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression de l\'utilisateur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreUserRequest.php b/thanasoft-back/app/Http/Requests/StoreUserRequest.php new file mode 100644 index 0000000..c032a08 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreUserRequest.php @@ -0,0 +1,28 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'], + 'password' => ['nullable', 'string', Password::min(8)], + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateEmployeeRequest.php b/thanasoft-back/app/Http/Requests/UpdateEmployeeRequest.php index 9f9784e..c0ecff7 100644 --- a/thanasoft-back/app/Http/Requests/UpdateEmployeeRequest.php +++ b/thanasoft-back/app/Http/Requests/UpdateEmployeeRequest.php @@ -23,8 +23,8 @@ class UpdateEmployeeRequest extends FormRequest public function rules(): array { return [ - 'first_name' => 'required|string|max:191', - 'last_name' => 'required|string|max:191', + 'first_name' => 'nullable|string|max:191', + 'last_name' => 'nullable|string|max:191', 'email' => [ 'nullable', 'email', @@ -32,6 +32,7 @@ class UpdateEmployeeRequest extends FormRequest Rule::unique('employees', 'email')->ignore($this->route('employee')) ], 'phone' => 'nullable|string|max:50', + 'user_id' => 'nullable|exists:users,id', 'job_title' => 'nullable|string|max:191', 'hire_date' => 'nullable|date', 'active' => 'boolean', @@ -56,6 +57,7 @@ class UpdateEmployeeRequest extends FormRequest 'email.unique' => 'Cette adresse email est déjà utilisée.', 'phone.string' => 'Le téléphone doit être une chaîne de caractères.', 'phone.max' => 'Le téléphone ne peut pas dépasser :max caractères.', + 'user_id.exists' => 'L\'utilisateur sélectionné est invalide.', 'job_title.string' => 'L\'intitulé du poste doit être une chaîne de caractères.', 'job_title.max' => 'L\'intitulé du poste ne peut pas dépasser :max caractères.', 'hire_date.date' => 'La date d\'embauche doit être une date valide.', diff --git a/thanasoft-back/app/Http/Requests/UpdateUserRequest.php b/thanasoft-back/app/Http/Requests/UpdateUserRequest.php new file mode 100644 index 0000000..676ddef --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateUserRequest.php @@ -0,0 +1,35 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique('users', 'email')->ignore($this->route('user')), + ], + 'password' => ['nullable', 'string', Password::min(8)], + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Employee/EmployeeResource.php b/thanasoft-back/app/Http/Resources/Employee/EmployeeResource.php index 84dc821..2f681d9 100644 --- a/thanasoft-back/app/Http/Resources/Employee/EmployeeResource.php +++ b/thanasoft-back/app/Http/Resources/Employee/EmployeeResource.php @@ -21,6 +21,7 @@ class EmployeeResource extends JsonResource 'full_name' => $this->full_name, 'email' => $this->email, 'phone' => $this->phone, + 'user_id' => $this->user_id, 'job_title' => $this->job_title, 'hire_date' => $this->hire_date?->format('Y-m-d'), 'active' => $this->active, @@ -32,6 +33,15 @@ class EmployeeResource extends JsonResource $this->relationLoaded('thanatopractitioner'), new ThanatopractitionerResource($this->thanatopractitioner) ), + 'user' => $this->when( + $this->relationLoaded('user') && $this->user, + fn () => [ + 'id' => $this->user->id, + 'name' => $this->user->name, + 'email' => $this->user->email, + 'employee_id' => $this->id, + ] + ), ]; } } diff --git a/thanasoft-back/app/Models/Employee.php b/thanasoft-back/app/Models/Employee.php index 9d0fe56..8550797 100644 --- a/thanasoft-back/app/Models/Employee.php +++ b/thanasoft-back/app/Models/Employee.php @@ -4,6 +4,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -19,6 +20,7 @@ class Employee extends Model protected $fillable = [ 'first_name', 'last_name', + 'user_id', 'email', 'phone', 'job_title', @@ -46,6 +48,11 @@ class Employee extends Model return $this->hasOne(Thanatopractitioner::class); } + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + /** * Get the full name of the employee. */ diff --git a/thanasoft-back/app/Models/User.php b/thanasoft-back/app/Models/User.php index 91135d7..893a1ea 100644 --- a/thanasoft-back/app/Models/User.php +++ b/thanasoft-back/app/Models/User.php @@ -4,6 +4,7 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; @@ -46,4 +47,9 @@ class User extends Authenticatable 'password' => 'hashed', ]; } + + public function employee(): HasOne + { + return $this->hasOne(Employee::class); + } } diff --git a/thanasoft-back/app/Providers/AppServiceProvider.php b/thanasoft-back/app/Providers/AppServiceProvider.php index 1ac5a1c..97ea76e 100644 --- a/thanasoft-back/app/Providers/AppServiceProvider.php +++ b/thanasoft-back/app/Providers/AppServiceProvider.php @@ -28,6 +28,10 @@ class AppServiceProvider extends ServiceProvider return new \App\Repositories\PriceListRepository($app->make(\App\Models\PriceList::class)); }); + $this->app->bind(\App\Repositories\UserRepositoryInterface::class, function ($app) { + return new \App\Repositories\UserRepository($app->make(\App\Models\User::class)); + }); + $this->app->bind(\App\Repositories\ClientContactRepositoryInterface::class, function ($app) { return new \App\Repositories\ClientContactRepository($app->make(\App\Models\ClientContact::class)); }); diff --git a/thanasoft-back/app/Repositories/EmployeeRepository.php b/thanasoft-back/app/Repositories/EmployeeRepository.php index 587153d..1d67a4f 100644 --- a/thanasoft-back/app/Repositories/EmployeeRepository.php +++ b/thanasoft-back/app/Repositories/EmployeeRepository.php @@ -48,7 +48,7 @@ class EmployeeRepository extends BaseRepository implements EmployeeRepositoryInt */ public function findById(int $id): ?Employee { - return $this->model->newQuery()->find($id); + return $this->model->newQuery()->with(['thanatopractitioner', 'user'])->find($id); } /** @@ -88,7 +88,7 @@ class EmployeeRepository extends BaseRepository implements EmployeeRepositoryInt */ public function getPaginated(int $perPage = 10): array { - $paginator = $this->model->newQuery()->paginate($perPage); + $paginator = $this->model->newQuery()->with(['thanatopractitioner', 'user'])->paginate($perPage); return [ 'employees' => $paginator->getCollection(), @@ -107,11 +107,18 @@ class EmployeeRepository extends BaseRepository implements EmployeeRepositoryInt public function getWithThanatopractitioner(): Collection { return $this->model->newQuery() - ->with('thanatopractitioner') + ->with(['thanatopractitioner', 'user']) ->orderBy('last_name') ->get(); } + public function find(int|string $id, array $columns = ['*']): ?\Illuminate\Database\Eloquent\Model + { + return $this->model->newQuery() + ->with(['thanatopractitioner', 'user']) + ->find($id, $columns); + } + /** * Get employee statistics. */ diff --git a/thanasoft-back/app/Repositories/UserRepository.php b/thanasoft-back/app/Repositories/UserRepository.php new file mode 100644 index 0000000..1a9efa4 --- /dev/null +++ b/thanasoft-back/app/Repositories/UserRepository.php @@ -0,0 +1,15 @@ +foreignId('user_id') + ->nullable() + ->after('id') + ->constrained('users') + ->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('employees', function (Blueprint $table) { + $table->dropForeign(['user_id']); + $table->dropColumn('user_id'); + }); + } +}; diff --git a/thanasoft-back/database/migrations/2026_04_08_113400_make_users_password_nullable.php b/thanasoft-back/database/migrations/2026_04_08_113400_make_users_password_nullable.php new file mode 100644 index 0000000..3bd91b0 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_04_08_113400_make_users_password_nullable.php @@ -0,0 +1,28 @@ +string('password')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->string('password')->nullable(false)->change(); + }); + } +}; diff --git a/thanasoft-back/routes/api.php b/thanasoft-back/routes/api.php index 9ef1773..3795d90 100644 --- a/thanasoft-back/routes/api.php +++ b/thanasoft-back/routes/api.php @@ -24,6 +24,7 @@ use App\Http\Controllers\Api\PurchaseOrderController; use App\Http\Controllers\Api\PriceListController; use App\Http\Controllers\Api\TvaRateController; use App\Http\Controllers\Api\GoodsReceiptController; +use App\Http\Controllers\Api\UserController; /* @@ -39,6 +40,8 @@ use App\Http\Controllers\Api\GoodsReceiptController; Route::prefix('auth')->group(function () { Route::post('/register', [AuthController::class, 'register']); Route::post('/login', [AuthController::class, 'login']); + Route::post('/check-email', [AuthController::class, 'checkEmail']); + Route::post('/create-password', [AuthController::class, 'createPasswordAndLogin']); Route::middleware('auth:sanctum')->group(function () { Route::get('/me', [AuthController::class, 'me']); @@ -59,6 +62,7 @@ Route::middleware('auth:sanctum')->group(function () { Route::post('client-groups/{id}/assign-clients', [ClientGroupController::class, 'assignClients']); Route::apiResource('client-groups', ClientGroupController::class); Route::apiResource('price-lists', PriceListController::class); + Route::apiResource('users', UserController::class); Route::apiResource('client-locations', ClientLocationController::class); Route::apiResource('client-locations', ClientLocationController::class); diff --git a/thanasoft-front/src/components/Organism/CRM/EmployeeDetailPresentation.vue b/thanasoft-front/src/components/Organism/CRM/EmployeeDetailPresentation.vue index b6eb2a0..bcabc12 100644 --- a/thanasoft-front/src/components/Organism/CRM/EmployeeDetailPresentation.vue +++ b/thanasoft-front/src/components/Organism/CRM/EmployeeDetailPresentation.vue @@ -1,23 +1,57 @@ + + diff --git a/thanasoft-front/src/components/Organism/CRM/employee/EmployeeDetailContent.vue b/thanasoft-front/src/components/Organism/CRM/employee/EmployeeDetailContent.vue index cf0fb5a..bb05099 100644 --- a/thanasoft-front/src/components/Organism/CRM/employee/EmployeeDetailContent.vue +++ b/thanasoft-front/src/components/Organism/CRM/employee/EmployeeDetailContent.vue @@ -1,7 +1,6 @@ +