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.
This commit is contained in:
parent
8f7019e815
commit
56b0c50111
@ -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
|
public function me(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
173
thanasoft-back/app/Http/Controllers/Api/UserController.php
Normal file
173
thanasoft-back/app/Http/Controllers/Api/UserController.php
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\StoreUserRequest;
|
||||||
|
use App\Http\Requests\UpdateUserRequest;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Repositories\UserRepositoryInterface;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class UserController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly UserRepositoryInterface $userRepository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$email = request()->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
thanasoft-back/app/Http/Requests/StoreUserRequest.php
Normal file
28
thanasoft-back/app/Http/Requests/StoreUserRequest.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
|
class StoreUserRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<int, string>|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)],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,8 +23,8 @@ class UpdateEmployeeRequest extends FormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'first_name' => 'required|string|max:191',
|
'first_name' => 'nullable|string|max:191',
|
||||||
'last_name' => 'required|string|max:191',
|
'last_name' => 'nullable|string|max:191',
|
||||||
'email' => [
|
'email' => [
|
||||||
'nullable',
|
'nullable',
|
||||||
'email',
|
'email',
|
||||||
@ -32,6 +32,7 @@ class UpdateEmployeeRequest extends FormRequest
|
|||||||
Rule::unique('employees', 'email')->ignore($this->route('employee'))
|
Rule::unique('employees', 'email')->ignore($this->route('employee'))
|
||||||
],
|
],
|
||||||
'phone' => 'nullable|string|max:50',
|
'phone' => 'nullable|string|max:50',
|
||||||
|
'user_id' => 'nullable|exists:users,id',
|
||||||
'job_title' => 'nullable|string|max:191',
|
'job_title' => 'nullable|string|max:191',
|
||||||
'hire_date' => 'nullable|date',
|
'hire_date' => 'nullable|date',
|
||||||
'active' => 'boolean',
|
'active' => 'boolean',
|
||||||
@ -56,6 +57,7 @@ class UpdateEmployeeRequest extends FormRequest
|
|||||||
'email.unique' => 'Cette adresse email est déjà utilisée.',
|
'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.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.',
|
'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.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.',
|
'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.',
|
'hire_date.date' => 'La date d\'embauche doit être une date valide.',
|
||||||
|
|||||||
35
thanasoft-back/app/Http/Requests/UpdateUserRequest.php
Normal file
35
thanasoft-back/app/Http/Requests/UpdateUserRequest.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
|
class UpdateUserRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<int, mixed>|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)],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,6 +21,7 @@ class EmployeeResource extends JsonResource
|
|||||||
'full_name' => $this->full_name,
|
'full_name' => $this->full_name,
|
||||||
'email' => $this->email,
|
'email' => $this->email,
|
||||||
'phone' => $this->phone,
|
'phone' => $this->phone,
|
||||||
|
'user_id' => $this->user_id,
|
||||||
'job_title' => $this->job_title,
|
'job_title' => $this->job_title,
|
||||||
'hire_date' => $this->hire_date?->format('Y-m-d'),
|
'hire_date' => $this->hire_date?->format('Y-m-d'),
|
||||||
'active' => $this->active,
|
'active' => $this->active,
|
||||||
@ -32,6 +33,15 @@ class EmployeeResource extends JsonResource
|
|||||||
$this->relationLoaded('thanatopractitioner'),
|
$this->relationLoaded('thanatopractitioner'),
|
||||||
new ThanatopractitionerResource($this->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,
|
||||||
|
]
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ class Employee extends Model
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'first_name',
|
'first_name',
|
||||||
'last_name',
|
'last_name',
|
||||||
|
'user_id',
|
||||||
'email',
|
'email',
|
||||||
'phone',
|
'phone',
|
||||||
'job_title',
|
'job_title',
|
||||||
@ -46,6 +48,11 @@ class Employee extends Model
|
|||||||
return $this->hasOne(Thanatopractitioner::class);
|
return $this->hasOne(Thanatopractitioner::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the full name of the employee.
|
* Get the full name of the employee.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -4,6 +4,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
@ -46,4 +47,9 @@ class User extends Authenticatable
|
|||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function employee(): HasOne
|
||||||
|
{
|
||||||
|
return $this->hasOne(Employee::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,10 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
return new \App\Repositories\PriceListRepository($app->make(\App\Models\PriceList::class));
|
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) {
|
$this->app->bind(\App\Repositories\ClientContactRepositoryInterface::class, function ($app) {
|
||||||
return new \App\Repositories\ClientContactRepository($app->make(\App\Models\ClientContact::class));
|
return new \App\Repositories\ClientContactRepository($app->make(\App\Models\ClientContact::class));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -48,7 +48,7 @@ class EmployeeRepository extends BaseRepository implements EmployeeRepositoryInt
|
|||||||
*/
|
*/
|
||||||
public function findById(int $id): ?Employee
|
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
|
public function getPaginated(int $perPage = 10): array
|
||||||
{
|
{
|
||||||
$paginator = $this->model->newQuery()->paginate($perPage);
|
$paginator = $this->model->newQuery()->with(['thanatopractitioner', 'user'])->paginate($perPage);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'employees' => $paginator->getCollection(),
|
'employees' => $paginator->getCollection(),
|
||||||
@ -107,11 +107,18 @@ class EmployeeRepository extends BaseRepository implements EmployeeRepositoryInt
|
|||||||
public function getWithThanatopractitioner(): Collection
|
public function getWithThanatopractitioner(): Collection
|
||||||
{
|
{
|
||||||
return $this->model->newQuery()
|
return $this->model->newQuery()
|
||||||
->with('thanatopractitioner')
|
->with(['thanatopractitioner', 'user'])
|
||||||
->orderBy('last_name')
|
->orderBy('last_name')
|
||||||
->get();
|
->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.
|
* Get employee statistics.
|
||||||
*/
|
*/
|
||||||
|
|||||||
15
thanasoft-back/app/Repositories/UserRepository.php
Normal file
15
thanasoft-back/app/Repositories/UserRepository.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class UserRepository extends BaseRepository implements UserRepositoryInterface
|
||||||
|
{
|
||||||
|
public function __construct(User $model)
|
||||||
|
{
|
||||||
|
parent::__construct($model);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
interface UserRepositoryInterface extends BaseRepositoryInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
<?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::table('employees', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
<?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::table('users', function (Blueprint $table) {
|
||||||
|
$table->string('password')->nullable()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->string('password')->nullable(false)->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -24,6 +24,7 @@ use App\Http\Controllers\Api\PurchaseOrderController;
|
|||||||
use App\Http\Controllers\Api\PriceListController;
|
use App\Http\Controllers\Api\PriceListController;
|
||||||
use App\Http\Controllers\Api\TvaRateController;
|
use App\Http\Controllers\Api\TvaRateController;
|
||||||
use App\Http\Controllers\Api\GoodsReceiptController;
|
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::prefix('auth')->group(function () {
|
||||||
Route::post('/register', [AuthController::class, 'register']);
|
Route::post('/register', [AuthController::class, 'register']);
|
||||||
Route::post('/login', [AuthController::class, 'login']);
|
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::middleware('auth:sanctum')->group(function () {
|
||||||
Route::get('/me', [AuthController::class, 'me']);
|
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::post('client-groups/{id}/assign-clients', [ClientGroupController::class, 'assignClients']);
|
||||||
Route::apiResource('client-groups', ClientGroupController::class);
|
Route::apiResource('client-groups', ClientGroupController::class);
|
||||||
Route::apiResource('price-lists', PriceListController::class);
|
Route::apiResource('price-lists', PriceListController::class);
|
||||||
|
Route::apiResource('users', UserController::class);
|
||||||
|
|
||||||
Route::apiResource('client-locations', ClientLocationController::class);
|
Route::apiResource('client-locations', ClientLocationController::class);
|
||||||
Route::apiResource('client-locations', ClientLocationController::class);
|
Route::apiResource('client-locations', ClientLocationController::class);
|
||||||
|
|||||||
@ -1,23 +1,57 @@
|
|||||||
<template>
|
<template>
|
||||||
<employee-detail-template>
|
<div class="edp">
|
||||||
<template #button-return>
|
<div class="edp__topbar">
|
||||||
<div class="col-12">
|
<div class="edp__topbar-left">
|
||||||
<router-link
|
<RouterLink to="/employes">
|
||||||
to="/employes"
|
<SoftButton color="secondary" variant="outline" size="sm">
|
||||||
class="btn btn-outline-secondary btn-sm mb-3"
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
>
|
>
|
||||||
<i class="fas fa-arrow-left me-2"></i>Retour aux employés
|
<path d="M10 3L5 8l5 5" />
|
||||||
</router-link>
|
</svg>
|
||||||
</div>
|
Retour
|
||||||
</template>
|
</SoftButton>
|
||||||
<template #loading-state>
|
</RouterLink>
|
||||||
<div v-if="isLoading" class="text-center p-5">
|
<div class="edp__breadcrumb">
|
||||||
<div class="spinner-border text-primary" role="status">
|
<span>Employes</span>
|
||||||
<span class="visually-hidden">Chargement...</span>
|
<span class="edp__breadcrumb-sep">/</span>
|
||||||
|
<span class="edp__breadcrumb-current">{{ employee.full_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<template #employee-detail-sidebar>
|
<div class="edp__topbar-actions">
|
||||||
|
<SoftButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
@click="activeTab = 'info'"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="13"
|
||||||
|
height="13"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
>
|
||||||
|
<path d="M11.5 2.5l2 2L5 13H3v-2L11.5 2.5z" />
|
||||||
|
</svg>
|
||||||
|
Modifier
|
||||||
|
</SoftButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isLoading" class="edp__state">
|
||||||
|
<div class="edp__spinner"></div>
|
||||||
|
<p>Chargement de l'employe...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="edp__body">
|
||||||
<EmployeeDetailSidebar
|
<EmployeeDetailSidebar
|
||||||
:avatar-url="employeeAvatar"
|
:avatar-url="employeeAvatar"
|
||||||
:initials="getInitials(employee.full_name)"
|
:initials="getInitials(employee.full_name)"
|
||||||
@ -28,20 +62,12 @@
|
|||||||
:active-tab="activeTab"
|
:active-tab="activeTab"
|
||||||
:is-active="employee.active"
|
:is-active="employee.active"
|
||||||
:is-thanatopractitioner="!!thanatopractitionerData"
|
:is-thanatopractitioner="!!thanatopractitionerData"
|
||||||
|
:employee="employee"
|
||||||
@edit-avatar="triggerFileInput"
|
@edit-avatar="triggerFileInput"
|
||||||
@change-tab="activeTab = $event"
|
@change-tab="activeTab = $event"
|
||||||
/>
|
/>
|
||||||
</template>
|
|
||||||
<template #file-input>
|
<div class="edp__panel">
|
||||||
<input
|
|
||||||
:ref="fileInput"
|
|
||||||
type="file"
|
|
||||||
class="d-none"
|
|
||||||
accept="image/*"
|
|
||||||
@change="handleAvatarUpload"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template #employee-detail-content>
|
|
||||||
<EmployeeDetailContent
|
<EmployeeDetailContent
|
||||||
:active-tab="activeTab"
|
:active-tab="activeTab"
|
||||||
:employee="employee"
|
:employee="employee"
|
||||||
@ -54,17 +80,28 @@
|
|||||||
@create-practitioner-document="handleCreatePractitionerDocument"
|
@create-practitioner-document="handleCreatePractitionerDocument"
|
||||||
@updating-practitioner-document="handleModifiedPractitionerDocument"
|
@updating-practitioner-document="handleModifiedPractitionerDocument"
|
||||||
@remove-practitioner-document="handleRemovePractitionerDocument"
|
@remove-practitioner-document="handleRemovePractitionerDocument"
|
||||||
|
@notify-success="emit('notify-success', $event)"
|
||||||
|
@notify-error="emit('notify-error', $event)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</div>
|
||||||
</employee-detail-template>
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
class="d-none"
|
||||||
|
accept="image/*"
|
||||||
|
@change="handleAvatarUpload"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits, ref } from "vue";
|
import { defineProps, defineEmits, ref } from "vue";
|
||||||
import EmployeeDetailTemplate from "@/components/templates/CRM/EmployeeDetailTemplate.vue";
|
import { RouterLink } from "vue-router";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
import EmployeeDetailSidebar from "./employee/EmployeeDetailSidebar.vue";
|
import EmployeeDetailSidebar from "./employee/EmployeeDetailSidebar.vue";
|
||||||
import EmployeeDetailContent from "./employee/EmployeeDetailContent.vue";
|
import EmployeeDetailContent from "./employee/EmployeeDetailContent.vue";
|
||||||
import { RouterLink } from "vue-router";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
employee: {
|
employee: {
|
||||||
@ -83,10 +120,6 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: "overview",
|
default: "overview",
|
||||||
},
|
},
|
||||||
fileInput: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
thanatopractitionerData: {
|
thanatopractitionerData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
@ -98,21 +131,30 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const localAvatar = ref(props.employeeAvatar);
|
const localAvatar = ref(props.employeeAvatar);
|
||||||
|
const fileInput = ref(null);
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
"updateTheEmployee",
|
"updateTheEmployee",
|
||||||
"create-practitioner-document",
|
"create-practitioner-document",
|
||||||
"updating-practitioner-document",
|
"updating-practitioner-document",
|
||||||
"remove-practitioner-document",
|
"remove-practitioner-document",
|
||||||
|
"notify-success",
|
||||||
|
"notify-error",
|
||||||
|
"update:activeTab",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const activeTab = ref(props.activeTab);
|
||||||
|
|
||||||
|
const triggerFileInput = () => {
|
||||||
|
fileInput.value?.click();
|
||||||
|
};
|
||||||
|
|
||||||
const handleAvatarUpload = (event) => {
|
const handleAvatarUpload = (event) => {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
localAvatar.value = e.target.result;
|
localAvatar.value = e.target.result;
|
||||||
// TODO: Upload to server
|
|
||||||
console.log("Upload avatar to server");
|
console.log("Upload avatar to server");
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
@ -149,11 +191,11 @@ const getEmployeeType = (employee) => {
|
|||||||
if (employee.thanatopractitioner) {
|
if (employee.thanatopractitioner) {
|
||||||
return "Thanatopractitioner";
|
return "Thanatopractitioner";
|
||||||
}
|
}
|
||||||
return "Employé";
|
return "Employe";
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString) => {
|
||||||
if (!dateString) return "Non renseignée";
|
if (!dateString) return "Non renseignee";
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return date.toLocaleDateString("fr-FR", {
|
return date.toLocaleDateString("fr-FR", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
@ -162,3 +204,96 @@ const formatDate = (dateString) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.edp {
|
||||||
|
padding: 1.5rem 1.75rem;
|
||||||
|
min-height: 100vh;
|
||||||
|
color: #344767;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edp__topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edp__topbar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edp__topbar-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edp__breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edp__breadcrumb-sep {
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edp__breadcrumb-current {
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edp__body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 280px 1fr;
|
||||||
|
gap: 1.25rem;
|
||||||
|
align-items: start;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edp__panel {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edp__state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
min-height: 420px;
|
||||||
|
text-align: center;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edp__spinner {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 2px solid #e5e7eb;
|
||||||
|
border-top-color: #111827;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: edp-spin 0.75s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes edp-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.edp {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edp__body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="ed-content">
|
||||||
<!-- Overview Tab -->
|
<div v-show="activeTab === 'overview'" class="ed-pane">
|
||||||
<div v-show="activeTab === 'overview'">
|
|
||||||
<EmployeeOverview
|
<EmployeeOverview
|
||||||
:employee="employee"
|
:employee="employee"
|
||||||
:formatted-hire-date="formattedHireDate"
|
:formatted-hire-date="formattedHireDate"
|
||||||
@ -10,16 +9,20 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Information Tab -->
|
<div v-show="activeTab === 'info'" class="ed-pane">
|
||||||
<div v-show="activeTab === 'info'">
|
<EmployeeInfoTab :employee="employee" @employee-updated="updateEmployee" />
|
||||||
<EmployeeInfoTab
|
</div>
|
||||||
|
|
||||||
|
<div v-show="activeTab === 'user'" class="ed-pane">
|
||||||
|
<EmployeeUserTab
|
||||||
:employee="employee"
|
:employee="employee"
|
||||||
@employee-updated="updateEmployee"
|
@employee-updated="updateEmployee"
|
||||||
|
@notify-success="$emit('notify-success', $event)"
|
||||||
|
@notify-error="$emit('notify-error', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Documents Tab -->
|
<div v-show="activeTab === 'documents'" class="ed-pane">
|
||||||
<div v-show="activeTab === 'documents'">
|
|
||||||
<EmployeeDocumentsTab
|
<EmployeeDocumentsTab
|
||||||
:documents="practitionerDocuments"
|
:documents="practitionerDocuments"
|
||||||
:employee-id="employee.id"
|
:employee-id="employee.id"
|
||||||
@ -29,16 +32,31 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Practitioner Tab (Only for thanatopractitioners) -->
|
<div v-show="activeTab === 'practitioner' && practitioner" class="ed-pane">
|
||||||
<div v-show="activeTab === 'practitioner' && thanatopractitionerData">
|
<div class="ed-card">
|
||||||
<!-- <EmployeePractitionerTab
|
<div class="ed-card__header">
|
||||||
:thanatopractitioner="thanatopractitionerData"
|
<span class="ed-card__title">Informations praticien</span>
|
||||||
:employee="employee"
|
</div>
|
||||||
/> -->
|
<div class="ed-card__body">
|
||||||
|
<div class="ed-grid">
|
||||||
|
<div class="ed-data">
|
||||||
|
<span class="ed-data__label">Numero de licence</span>
|
||||||
|
<strong class="ed-data__value">{{ practitioner?.license_number || 'Non renseigne' }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="ed-data">
|
||||||
|
<span class="ed-data__label">Numero d'autorisation</span>
|
||||||
|
<strong class="ed-data__value">{{ practitioner?.authorization_number || 'Non renseigne' }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="ed-data">
|
||||||
|
<span class="ed-data__label">Validite</span>
|
||||||
|
<strong class="ed-data__value">{{ formatDate(practitioner?.authorization_valid_until) }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Activity Tab -->
|
<div v-show="activeTab === 'activity'" class="ed-pane">
|
||||||
<div v-show="activeTab === 'activity'">
|
|
||||||
<EmployeeActivityTab :employee="employee" />
|
<EmployeeActivityTab :employee="employee" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -47,12 +65,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import EmployeeOverview from "@/components/molecules/employee/EmployeeOverview.vue";
|
import EmployeeOverview from "@/components/molecules/employee/EmployeeOverview.vue";
|
||||||
import EmployeeInfoTab from "@/components/molecules/employee/EmployeeInfoTab.vue";
|
import EmployeeInfoTab from "@/components/molecules/employee/EmployeeInfoTab.vue";
|
||||||
|
import EmployeeUserTab from "@/components/molecules/employee/EmployeeUserTab.vue";
|
||||||
import EmployeeDocumentsTab from "@/components/molecules/employee/EmployeeDocumentsTab.vue";
|
import EmployeeDocumentsTab from "@/components/molecules/employee/EmployeeDocumentsTab.vue";
|
||||||
import EmployeePractitionerTab from "@/components/molecules/employee/EmployeePractitionerTab.vue";
|
import { computed, defineProps, defineEmits } from "vue";
|
||||||
import { defineProps, defineEmits } from "vue";
|
|
||||||
import EmployeeActivityTab from "@/components/molecules/employee/EmployeeActivityTab.vue";
|
import EmployeeActivityTab from "@/components/molecules/employee/EmployeeActivityTab.vue";
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
activeTab: {
|
activeTab: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
@ -79,12 +97,18 @@ defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const practitioner = computed(
|
||||||
|
() => props.thanatopractitionerData || props.employee?.thanatopractitioner || null
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
"change-tab",
|
"change-tab",
|
||||||
"updating-employee",
|
"updating-employee",
|
||||||
"create-practitioner-document",
|
"create-practitioner-document",
|
||||||
"updating-practitioner-document",
|
"updating-practitioner-document",
|
||||||
"remove-practitioner-document",
|
"remove-practitioner-document",
|
||||||
|
"notify-success",
|
||||||
|
"notify-error",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const updateEmployee = (updatedEmployee) => {
|
const updateEmployee = (updatedEmployee) => {
|
||||||
@ -102,4 +126,80 @@ const handleModifiedDocument = (modifiedDocument) => {
|
|||||||
const handleRemoveDocument = (documentId) => {
|
const handleRemoveDocument = (documentId) => {
|
||||||
emit("remove-practitioner-document", documentId);
|
emit("remove-practitioner-document", documentId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return "Non renseignee";
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString("fr-FR", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ed-content {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ed-pane {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ed-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(131, 146, 171, 0.25);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ed-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.875rem 1.25rem;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ed-card__title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #8392ab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ed-card__body {
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ed-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ed-data {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.85rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #eef2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ed-data__label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #8392ab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ed-data__value {
|
||||||
|
color: #344767;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -1,37 +1,63 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card position-sticky top-1">
|
<aside class="product-sidebar">
|
||||||
<!-- Employee Profile Card -->
|
<div class="product-sidebar__img-wrap" @click="$emit('edit-avatar')">
|
||||||
<EmployeeProfileCard
|
<img
|
||||||
:avatar-url="avatarUrl"
|
v-if="avatarUrl"
|
||||||
:initials="initials"
|
:src="avatarUrl"
|
||||||
:employee-name="employeeName"
|
:alt="employeeName"
|
||||||
:job-title="jobTitle"
|
class="employee-sidebar__avatar"
|
||||||
:status="status"
|
|
||||||
:hire-date="hireDate"
|
|
||||||
:is-active="isActive"
|
|
||||||
:is-thanatopractitioner="isThanatopractitioner"
|
|
||||||
@edit-avatar="$emit('edit-avatar')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<hr class="horizontal dark my-3 mx-3" />
|
|
||||||
|
|
||||||
<!-- Tab Navigation -->
|
|
||||||
<div class="card-body pt-0">
|
|
||||||
<EmployeeTabNavigation
|
|
||||||
:active-tab="activeTab"
|
|
||||||
:is-thanatopractitioner="isThanatopractitioner"
|
|
||||||
@change-tab="$emit('change-tab', $event)"
|
|
||||||
/>
|
/>
|
||||||
|
<div v-else class="employee-sidebar__avatar employee-sidebar__avatar--fallback">
|
||||||
|
{{ initials }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="product-sidebar__meta">
|
||||||
|
<h6 class="product-sidebar__name">{{ employeeName }}</h6>
|
||||||
|
<p class="product-sidebar__ref">{{ jobTitle || status || "Employe" }}</p>
|
||||||
|
<div class="product-sidebar__badges employee-sidebar__badges">
|
||||||
|
<span
|
||||||
|
class="employee-sidebar__badge"
|
||||||
|
:class="isActive ? 'employee-sidebar__badge--success' : 'employee-sidebar__badge--muted'"
|
||||||
|
>
|
||||||
|
{{ isActive ? "Actif" : "Inactif" }}
|
||||||
|
</span>
|
||||||
|
<span v-if="isThanatopractitioner" class="employee-sidebar__badge employee-sidebar__badge--info">
|
||||||
|
Thanatopracteur
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="employee-sidebar__details">
|
||||||
|
<div class="employee-sidebar__detail-item">
|
||||||
|
<span class="employee-sidebar__detail-label">Embauche</span>
|
||||||
|
<span class="employee-sidebar__detail-value">{{ hireDate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="employee-sidebar__detail-item">
|
||||||
|
<span class="employee-sidebar__detail-label">Contact</span>
|
||||||
|
<span class="employee-sidebar__detail-value">{{ employee.email || employee.phone || "Non renseigne" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="product-sidebar__nav">
|
||||||
|
<button
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.id"
|
||||||
|
class="product-sidebar__nav-item"
|
||||||
|
:class="{ 'is-active': activeTab === tab.id }"
|
||||||
|
@click="$emit('change-tab', tab.id)"
|
||||||
|
>
|
||||||
|
<component :is="tab.icon" class="nav-icon" />
|
||||||
|
{{ tab.label }}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import EmployeeProfileCard from "@/components/molecules/employee/EmployeeProfileCard.vue";
|
import { computed, defineComponent, defineProps, defineEmits, h } from "vue";
|
||||||
import EmployeeTabNavigation from "@/components/molecules/employee/EmployeeTabNavigation.vue";
|
|
||||||
import { defineProps, defineEmits } from "vue";
|
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
avatarUrl: {
|
avatarUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
@ -46,7 +72,7 @@ defineProps({
|
|||||||
},
|
},
|
||||||
jobTitle: {
|
jobTitle: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "Employé",
|
default: "Employe",
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -68,18 +94,292 @@ defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
employee: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(["edit-avatar", "change-tab"]);
|
defineEmits(["edit-avatar", "change-tab"]);
|
||||||
|
|
||||||
|
const IconOverview = defineComponent({
|
||||||
|
render: () =>
|
||||||
|
h(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
viewBox: "0 0 16 16",
|
||||||
|
fill: "none",
|
||||||
|
stroke: "currentColor",
|
||||||
|
"stroke-width": "1.5",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h("path", { d: "M2 8s2.5-4 6-4 6 4 6 4-2.5 4-6 4-6-4-6-4z" }),
|
||||||
|
h("circle", { cx: "8", cy: "8", r: "1.75" }),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const IconInfo = defineComponent({
|
||||||
|
render: () =>
|
||||||
|
h(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
viewBox: "0 0 16 16",
|
||||||
|
fill: "none",
|
||||||
|
stroke: "currentColor",
|
||||||
|
"stroke-width": "1.5",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h("circle", { cx: "8", cy: "8", r: "6" }),
|
||||||
|
h("path", { d: "M8 7v3M8 5.25h.01" }),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const IconDocument = defineComponent({
|
||||||
|
render: () =>
|
||||||
|
h(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
viewBox: "0 0 16 16",
|
||||||
|
fill: "none",
|
||||||
|
stroke: "currentColor",
|
||||||
|
"stroke-width": "1.5",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h("path", { d: "M5 2.5h4l2.5 2.5v7A1.5 1.5 0 0 1 10 13.5H5A1.5 1.5 0 0 1 3.5 12V4A1.5 1.5 0 0 1 5 2.5z" }),
|
||||||
|
h("path", { d: "M9 2.5V5h2.5" }),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const IconPractitioner = defineComponent({
|
||||||
|
render: () =>
|
||||||
|
h(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
viewBox: "0 0 16 16",
|
||||||
|
fill: "none",
|
||||||
|
stroke: "currentColor",
|
||||||
|
"stroke-width": "1.5",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h("circle", { cx: "8", cy: "5", r: "2.5" }),
|
||||||
|
h("path", { d: "M3.5 13c.5-2.3 2.3-3.5 4.5-3.5s4 1.2 4.5 3.5" }),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const IconActivity = defineComponent({
|
||||||
|
render: () =>
|
||||||
|
h(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
viewBox: "0 0 16 16",
|
||||||
|
fill: "none",
|
||||||
|
stroke: "currentColor",
|
||||||
|
"stroke-width": "1.5",
|
||||||
|
},
|
||||||
|
[h("path", { d: "M2.5 11.5h2l1.5-3 2.25 4 1.75-3h3.5" })]
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabs = computed(() => {
|
||||||
|
const baseTabs = [
|
||||||
|
{ id: "overview", label: "Apercu", icon: IconOverview },
|
||||||
|
{ id: "info", label: "Informations", icon: IconInfo },
|
||||||
|
{ id: "user", label: "Utilisateur", icon: IconPractitioner },
|
||||||
|
{ id: "documents", label: "Documents", icon: IconDocument },
|
||||||
|
{ id: "activity", label: "Activite", icon: IconActivity },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (props.isThanatopractitioner) {
|
||||||
|
baseTabs.splice(3, 0, {
|
||||||
|
id: "practitioner",
|
||||||
|
label: "Praticien",
|
||||||
|
icon: IconPractitioner,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseTabs;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.position-sticky {
|
.product-sidebar {
|
||||||
top: 1rem;
|
position: sticky;
|
||||||
|
top: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.product-sidebar__img-wrap {
|
||||||
border: 0;
|
width: 72px;
|
||||||
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
|
height: 72px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
margin: 1.25rem auto 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-sidebar__avatar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-sidebar__avatar--fallback {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(310deg, #5e72e4 0%, #825ee4 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sidebar__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 0.75rem 1rem 1rem;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sidebar__name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sidebar__ref {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sidebar__badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-sidebar__badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0.2rem 0.55rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-sidebar__badge--success {
|
||||||
|
background: #ecfdf3;
|
||||||
|
color: #047857;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-sidebar__badge--muted {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-sidebar__badge--info {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-sidebar__details {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.9rem 1rem 1rem;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-sidebar__detail-item {
|
||||||
|
padding: 0.7rem 0.8rem;
|
||||||
|
border: 1px solid #eef2f7;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-sidebar__detail-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #8392ab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-sidebar__detail-value {
|
||||||
|
display: block;
|
||||||
|
color: #344767;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sidebar__nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.5rem;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sidebar__nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
border-radius: 7px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6b7280;
|
||||||
|
font-family: inherit;
|
||||||
|
text-align: left;
|
||||||
|
transition: background 0.12s, color 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sidebar__nav-item:hover {
|
||||||
|
background: #f9fafb;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sidebar__nav-item.is-active {
|
||||||
|
background: #111827;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.product-sidebar {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -70,7 +70,7 @@
|
|||||||
<!-- Main Layout -->
|
<!-- Main Layout -->
|
||||||
<div v-else-if="mappedIntervention" class="page-layout">
|
<div v-else-if="mappedIntervention" class="page-layout">
|
||||||
<!-- LEFT SIDEBAR -->
|
<!-- LEFT SIDEBAR -->
|
||||||
<aside class="sidebar">
|
<aside class="sidebar card position-sticky top-1">
|
||||||
<!-- Hero Card -->
|
<!-- Hero Card -->
|
||||||
<div class="hero-card">
|
<div class="hero-card">
|
||||||
<div class="hero-avatar">
|
<div class="hero-avatar">
|
||||||
@ -89,10 +89,14 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-info">
|
<div class="hero-info">
|
||||||
<h2 class="hero-name">{{ mappedIntervention.defuntName }}</h2>
|
<h5 class="hero-name font-weight-bolder mb-0">
|
||||||
<p class="hero-type">{{ mappedIntervention.title }}</p>
|
{{ mappedIntervention.defuntName }}
|
||||||
|
</h5>
|
||||||
|
<p class="hero-type text-sm text-secondary mb-3">
|
||||||
|
{{ mappedIntervention.title }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<StatusPill :status="mappedIntervention.status" large />
|
<StatusPill :status="mappedIntervention.status" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick Stats -->
|
<!-- Quick Stats -->
|
||||||
@ -115,7 +119,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="stat-label">Date</div>
|
<div class="stat-label">Date</div>
|
||||||
<div class="stat-value">{{ mappedIntervention.date }}</div>
|
<div class="stat-value">
|
||||||
|
{{ mappedIntervention.date }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
@ -134,7 +140,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="stat-label">Lieu</div>
|
<div class="stat-label">Lieu</div>
|
||||||
<div class="stat-value">{{ mappedIntervention.lieux }}</div>
|
<div class="stat-value">
|
||||||
|
{{ mappedIntervention.lieux }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
@ -153,25 +161,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="stat-label">Durée</div>
|
<div class="stat-label">Durée</div>
|
||||||
<div class="stat-value">{{ mappedIntervention.duree }}</div>
|
<div class="stat-value">
|
||||||
|
{{ mappedIntervention.duree }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab Navigation -->
|
<!-- Tab Navigation -->
|
||||||
<nav class="sidebar-nav">
|
<InterventionTabNavigation
|
||||||
<button
|
class="sidebar-nav"
|
||||||
v-for="tab in tabs"
|
:active-tab="localActiveTab"
|
||||||
:key="tab.id"
|
:team-count="mappedIntervention.practitioners?.length || 0"
|
||||||
class="nav-item"
|
:documents-count="documentAttachments.length"
|
||||||
:class="{ active: localActiveTab === tab.id }"
|
@change-tab="changeTab"
|
||||||
@click="changeTab(tab.id)"
|
/>
|
||||||
>
|
|
||||||
<span class="nav-icon" v-html="tab.icon"></span>
|
|
||||||
<span class="nav-label">{{ tab.label }}</span>
|
|
||||||
<span v-if="tab.badge" class="nav-badge">{{ tab.badge }}</span>
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Assign Button -->
|
<!-- Assign Button -->
|
||||||
<button class="assign-btn" @click="$emit('assign-practitioner')">
|
<button class="assign-btn" @click="$emit('assign-practitioner')">
|
||||||
@ -629,6 +633,7 @@ import AssignPractitionerModal from "@/components/molecules/intervention/AssignP
|
|||||||
import StatusPill from "@/components/molecules/intervention/StatusPill.vue";
|
import StatusPill from "@/components/molecules/intervention/StatusPill.vue";
|
||||||
import InfoSection from "@/components/molecules/intervention/InfoSection.vue";
|
import InfoSection from "@/components/molecules/intervention/InfoSection.vue";
|
||||||
import DataRow from "@/components/molecules/intervention/DataRow.vue";
|
import DataRow from "@/components/molecules/intervention/DataRow.vue";
|
||||||
|
import InterventionTabNavigation from "@/components/molecules/intervention/InterventionTabNavigation.vue";
|
||||||
import { useDocumentAttachmentStore } from "@/stores/documentAttachmentStore";
|
import { useDocumentAttachmentStore } from "@/stores/documentAttachmentStore";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -698,28 +703,6 @@ const mappedIntervention = computed(() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const tabs = computed(() => [
|
|
||||||
{ id: "overview", label: "Vue d'ensemble", icon: eyeIcon },
|
|
||||||
{ id: "details", label: "Détails", icon: listIcon },
|
|
||||||
{
|
|
||||||
id: "team",
|
|
||||||
label: "Équipe",
|
|
||||||
icon: teamIcon,
|
|
||||||
badge: mappedIntervention.value?.practitioners?.length || null,
|
|
||||||
},
|
|
||||||
{ id: "documents", label: "Documents", icon: docIcon },
|
|
||||||
{ id: "quote", label: "Devis", icon: quoteIcon },
|
|
||||||
{ id: "history", label: "Historique", icon: histIcon },
|
|
||||||
]);
|
|
||||||
|
|
||||||
// SVG icons as strings for tab nav
|
|
||||||
const eyeIcon = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>`;
|
|
||||||
const listIcon = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>`;
|
|
||||||
const teamIcon = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>`;
|
|
||||||
const docIcon = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`;
|
|
||||||
const quoteIcon = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>`;
|
|
||||||
const histIcon = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-4.5"/></svg>`;
|
|
||||||
|
|
||||||
// ── Helpers ──
|
// ── Helpers ──
|
||||||
const getStatusLabel = (s) =>
|
const getStatusLabel = (s) =>
|
||||||
({
|
({
|
||||||
@ -870,18 +853,7 @@ onMounted(() => {
|
|||||||
/* ─── Page Shell ────────────────────────────────────────────────── */
|
/* ─── Page Shell ────────────────────────────────────────────────── */
|
||||||
.intervention-page {
|
.intervention-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background:
|
background: var(--bs-body-bg);
|
||||||
radial-gradient(
|
|
||||||
circle at 8% 8%,
|
|
||||||
rgba(99, 102, 241, 0.12) 0%,
|
|
||||||
rgba(99, 102, 241, 0) 34%
|
|
||||||
),
|
|
||||||
radial-gradient(
|
|
||||||
circle at 92% 4%,
|
|
||||||
rgba(16, 185, 129, 0.09) 0%,
|
|
||||||
rgba(16, 185, 129, 0) 30%
|
|
||||||
),
|
|
||||||
linear-gradient(180deg, #f8fbff 0%, #f2f6fd 100%);
|
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -947,25 +919,27 @@ onMounted(() => {
|
|||||||
|
|
||||||
/* ─── Sidebar ───────────────────────────────────────────────────── */
|
/* ─── Sidebar ───────────────────────────────────────────────────── */
|
||||||
.sidebar {
|
.sidebar {
|
||||||
background: var(--surface);
|
background: #fff;
|
||||||
border-right: 1px solid var(--border);
|
border: 0;
|
||||||
|
border-right: 0;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
position: sticky;
|
top: 1rem;
|
||||||
top: 57px;
|
|
||||||
height: calc(100vh - 57px);
|
height: calc(100vh - 57px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-card {
|
.hero-card {
|
||||||
padding: 24px 20px 20px;
|
padding: 1.5rem 1.5rem 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 0.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-bottom: 1px solid var(--border-light);
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-avatar {
|
.hero-avatar {
|
||||||
@ -982,26 +956,26 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero-name {
|
.hero-name {
|
||||||
font-size: 16px;
|
font-size: 1.25rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: #344767;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
.hero-type {
|
.hero-type {
|
||||||
font-size: 12px;
|
font-size: 0.875rem;
|
||||||
color: var(--text-secondary);
|
color: #67748e;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: 500;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Quick Stats */
|
/* Quick Stats */
|
||||||
.quick-stats {
|
.quick-stats {
|
||||||
padding: 16px 20px;
|
padding: 0 1.5rem 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 0.75rem;
|
||||||
border-bottom: 1px solid var(--border-light);
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
.stat-item {
|
.stat-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -1031,78 +1005,24 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 11px;
|
font-size: 0.75rem;
|
||||||
color: var(--text-muted);
|
color: #8392ab;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.4px;
|
letter-spacing: 0.4px;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 13px;
|
font-size: 0.875rem;
|
||||||
color: var(--text-primary);
|
color: #344767;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
margin-top: 1px;
|
margin-top: 0.1rem;
|
||||||
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar Nav */
|
/* Sidebar Nav */
|
||||||
.sidebar-nav {
|
.sidebar-nav {
|
||||||
padding: 12px 12px;
|
padding: 0 1rem 1rem;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
.nav-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 9px 12px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 13.5px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
transition: all 0.15s;
|
|
||||||
}
|
|
||||||
.nav-item:hover {
|
|
||||||
background: var(--surface-3);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
.nav-item.active {
|
|
||||||
background: var(--brand-light);
|
|
||||||
color: var(--brand);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.nav-item.active .nav-icon {
|
|
||||||
color: var(--brand);
|
|
||||||
}
|
|
||||||
.nav-icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
color: var(--text-muted);
|
|
||||||
transition: color 0.15s;
|
|
||||||
}
|
|
||||||
.nav-label {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.nav-badge {
|
|
||||||
min-width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
padding: 0 6px;
|
|
||||||
background: var(--brand);
|
|
||||||
color: white;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.nav-item.active .nav-badge {
|
|
||||||
background: var(--brand-dark);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Assign Button */
|
/* Assign Button */
|
||||||
@ -1630,8 +1550,7 @@ onMounted(() => {
|
|||||||
.sidebar {
|
.sidebar {
|
||||||
position: static;
|
position: static;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-right: none;
|
border-bottom: 0;
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
}
|
||||||
.sidebar-nav {
|
.sidebar-nav {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@ -1,258 +1,134 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div class="eo">
|
||||||
|
<div class="eo-card eo-card--hero">
|
||||||
|
<div class="eo-card__body">
|
||||||
|
<div class="eo-hero">
|
||||||
<div>
|
<div>
|
||||||
<!-- Header -->
|
<p class="eo-eyebrow">Apercu collaborateur</p>
|
||||||
<div class="row mb-4">
|
<h2 class="eo-title">{{ employee.full_name }}</h2>
|
||||||
<div class="col-12">
|
<p class="eo-subtitle">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
{{ employee.job_title || "Poste non renseigne" }}
|
||||||
<h6 class="mb-0">Aperçu de l'employé</h6>
|
</p>
|
||||||
<div>
|
</div>
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-outline-primary"
|
<button class="eo-action" type="button" @click="$emit('view-info-tab')">
|
||||||
@click="$emit('view-info-tab')"
|
<i class="fas fa-pen"></i>
|
||||||
>
|
Modifier la fiche
|
||||||
<i class="fas fa-edit me-1"></i>
|
|
||||||
Modifier
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="eo-highlights">
|
||||||
|
<div class="eo-highlight">
|
||||||
|
<span class="eo-highlight__label">Email</span>
|
||||||
|
<strong class="eo-highlight__value">{{ employee.email || "Non renseigne" }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="eo-highlight">
|
||||||
|
<span class="eo-highlight__label">Telephone</span>
|
||||||
|
<strong class="eo-highlight__value">{{ employee.phone || "Non renseigne" }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="eo-highlight">
|
||||||
|
<span class="eo-highlight__label">Statut</span>
|
||||||
|
<strong class="eo-highlight__value">{{ employee.active ? "Actif" : "Inactif" }}</strong>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Basic Information Cards -->
|
<div class="eo-grid mt-3">
|
||||||
<div class="row">
|
<div class="eo-card">
|
||||||
<div class="col-md-6 mb-4">
|
<div class="eo-card__header">
|
||||||
<div class="card card-stats card-round">
|
<span class="eo-card__title">Coordonnees</span>
|
||||||
<div class="card-body">
|
</div>
|
||||||
<div class="row align-items-center">
|
<div class="eo-card__body">
|
||||||
<div class="col-icon">
|
<div class="eo-list">
|
||||||
<div
|
<div class="eo-list__item">
|
||||||
class="icon-big text-center icon-warning bubble-shadow-small"
|
<span class="eo-list__label">Prenom</span>
|
||||||
>
|
<strong class="eo-list__value">{{ employee.first_name || "Non renseigne" }}</strong>
|
||||||
<i class="fas fa-user"></i>
|
</div>
|
||||||
|
<div class="eo-list__item">
|
||||||
|
<span class="eo-list__label">Nom</span>
|
||||||
|
<strong class="eo-list__value">{{ employee.last_name || "Non renseigne" }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="eo-list__item">
|
||||||
|
<span class="eo-list__label">Email</span>
|
||||||
|
<strong class="eo-list__value">{{ employee.email || "Non renseigne" }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="eo-list__item">
|
||||||
|
<span class="eo-list__label">Telephone</span>
|
||||||
|
<strong class="eo-list__value">{{ employee.phone || "Non renseigne" }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-stats ms-3 ms-sm-0 mt-3">
|
|
||||||
<div class="numbers">
|
|
||||||
<p class="card-category">Prénom</p>
|
|
||||||
<h4 class="card-title">{{ employee.first_name }}</h4>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="eo-card">
|
||||||
|
<div class="eo-card__header">
|
||||||
|
<span class="eo-card__title">Informations d'emploi</span>
|
||||||
|
</div>
|
||||||
|
<div class="eo-card__body">
|
||||||
|
<div class="eo-list">
|
||||||
|
<div class="eo-list__item">
|
||||||
|
<span class="eo-list__label">Date d'embauche</span>
|
||||||
|
<strong class="eo-list__value">{{ formattedHireDate }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="eo-list__item">
|
||||||
|
<span class="eo-list__label">Poste</span>
|
||||||
|
<strong class="eo-list__value">{{ employee.job_title || "Non renseigne" }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="eo-list__item">
|
||||||
|
<span class="eo-list__label">Salaire</span>
|
||||||
|
<strong class="eo-list__value">{{ employee.salary ? `${employee.salary} EUR` : "Non renseigne" }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="eo-list__item">
|
||||||
|
<span class="eo-list__label">Type</span>
|
||||||
|
<strong class="eo-list__value">{{ employee.thanatopractitioner ? "Thanatopracteur" : "Employe" }}</strong>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 mb-4">
|
<div v-if="employee.thanatopractitioner" class="eo-card mt-3">
|
||||||
<div class="card card-stats card-round">
|
<div class="eo-card__header">
|
||||||
<div class="card-body">
|
<span class="eo-card__title">Certification praticien</span>
|
||||||
<div class="row align-items-center">
|
|
||||||
<div class="col-icon">
|
|
||||||
<div
|
|
||||||
class="icon-big text-center icon-warning bubble-shadow-small"
|
|
||||||
>
|
|
||||||
<i class="fas fa-user"></i>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="eo-card__body">
|
||||||
|
<div class="eo-grid eo-grid--triple">
|
||||||
|
<div class="eo-list__item">
|
||||||
|
<span class="eo-list__label">Numero de licence</span>
|
||||||
|
<strong class="eo-list__value">
|
||||||
|
{{ employee.thanatopractitioner.license_number || "Non renseigne" }}
|
||||||
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-stats ms-3 ms-sm-0 mt-3">
|
<div class="eo-list__item">
|
||||||
<div class="numbers">
|
<span class="eo-list__label">Numero d'autorisation</span>
|
||||||
<p class="card-category">Nom</p>
|
<strong class="eo-list__value">
|
||||||
<h4 class="card-title">{{ employee.last_name }}</h4>
|
{{ employee.thanatopractitioner.authorization_number || "Non renseigne" }}
|
||||||
</div>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="eo-list__item">
|
||||||
|
<span class="eo-list__label">Validite</span>
|
||||||
|
<strong class="eo-list__value">
|
||||||
|
{{ formatDate(employee.thanatopractitioner.authorization_valid_until) }}
|
||||||
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 mb-4">
|
<div class="eo-card mt-3">
|
||||||
<div class="card card-stats card-round">
|
<div class="eo-card__header">
|
||||||
<div class="card-body">
|
<span class="eo-card__title">Informations systeme</span>
|
||||||
<div class="row align-items-center">
|
|
||||||
<div class="col-icon">
|
|
||||||
<div
|
|
||||||
class="icon-big text-center icon-success bubble-shadow-small"
|
|
||||||
>
|
|
||||||
<i class="fas fa-envelope"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col col-stats ms-3 ms-sm-0 mt-3">
|
|
||||||
<div class="numbers">
|
|
||||||
<p class="card-category">Email</p>
|
|
||||||
<h4 class="card-title">
|
|
||||||
{{ employee.email || "Non renseigné" }}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 mb-4">
|
|
||||||
<div class="card card-stats card-round">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row align-items-center">
|
|
||||||
<div class="col-icon">
|
|
||||||
<div class="icon-big text-center icon-info bubble-shadow-small">
|
|
||||||
<i class="fas fa-phone"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col col-stats ms-3 ms-sm-0 mt-3">
|
|
||||||
<div class="numbers">
|
|
||||||
<p class="card-category">Téléphone</p>
|
|
||||||
<h4 class="card-title">
|
|
||||||
{{ employee.phone || "Non renseigné" }}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Employment Information -->
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 mb-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<div class="card-head-row">
|
|
||||||
<div class="card-title">Informations d'emploi</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Date d'embauche</label>
|
|
||||||
<p class="form-control-static">{{ formattedHireDate }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Poste occupé</label>
|
|
||||||
<p class="form-control-static">
|
|
||||||
{{ employee.job_title || "Non renseigné" }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Salaire</label>
|
|
||||||
<p class="form-control-static">
|
|
||||||
{{
|
|
||||||
employee.salary ? `${employee.salary} €` : "Non renseigné"
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Statut</label>
|
|
||||||
<p class="form-control-static">
|
|
||||||
<span
|
|
||||||
:class="
|
|
||||||
employee.active
|
|
||||||
? 'badge bg-success'
|
|
||||||
: 'badge bg-secondary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ employee.active ? "Actif" : "Inactif" }}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Specialization for Thanatopractitioners -->
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
employee.thanatopractitioner &&
|
|
||||||
employee.thanatopractitioner.license_number
|
|
||||||
"
|
|
||||||
class="row"
|
|
||||||
>
|
|
||||||
<div class="col-12 mb-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<div class="card-head-row">
|
|
||||||
<div class="card-title">Certification Thanatopractitioner</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Numéro de licence</label>
|
|
||||||
<p class="form-control-static">
|
|
||||||
{{
|
|
||||||
employee.thanatopractitioner.license_number ||
|
|
||||||
"Non renseigné"
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Numéro d'autorisation</label>
|
|
||||||
<p class="form-control-static">
|
|
||||||
{{
|
|
||||||
employee.thanatopractitioner.authorization_number ||
|
|
||||||
"Non renseigné"
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Validité de l'autorisation</label>
|
|
||||||
<p class="form-control-static">
|
|
||||||
{{
|
|
||||||
formatDate(
|
|
||||||
employee.thanatopractitioner.authorization_valid_until
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- System Information -->
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<div class="card-head-row">
|
|
||||||
<div class="card-title">Informations système</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Date de création</label>
|
|
||||||
<p class="form-control-static">
|
|
||||||
{{ formatDate(employee.created_at) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Dernière modification</label>
|
|
||||||
<p class="form-control-static">
|
|
||||||
{{ formatDate(employee.updated_at) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="eo-card__body">
|
||||||
|
<div class="eo-grid">
|
||||||
|
<div class="eo-list__item">
|
||||||
|
<span class="eo-list__label">Date de creation</span>
|
||||||
|
<strong class="eo-list__value">{{ formatDate(employee.created_at) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="eo-list__item">
|
||||||
|
<span class="eo-list__label">Derniere modification</span>
|
||||||
|
<strong class="eo-list__value">{{ formatDate(employee.updated_at) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -278,10 +154,10 @@ defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["view-info-tab"]);
|
defineEmits(["view-info-tab"]);
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString) => {
|
||||||
if (!dateString) return "Non renseignée";
|
if (!dateString) return "Non renseignee";
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return date.toLocaleDateString("fr-FR", {
|
return date.toLocaleDateString("fr-FR", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
@ -294,53 +170,127 @@ const formatDate = (dateString) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.card-stats {
|
.eo {
|
||||||
border: none;
|
min-width: 0;
|
||||||
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.075);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.eo-card {
|
||||||
font-size: 1.1rem;
|
background: #fff;
|
||||||
margin-bottom: 0;
|
border: 1px solid rgba(131, 146, 171, 0.25);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-category {
|
.eo-card--hero {
|
||||||
font-size: 0.875rem;
|
border-color: rgba(94, 114, 228, 0.2);
|
||||||
color: #6c757d;
|
background: linear-gradient(135deg, #f8f9ff 0%, #ffffff 100%);
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-label {
|
.eo-card__header {
|
||||||
font-weight: 600;
|
|
||||||
color: #495057;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control-static {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
color: #212529;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-big {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble-shadow-small {
|
|
||||||
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.col-icon {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: 4.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-head-row {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.875rem 1.25rem;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eo-card__title,
|
||||||
|
.eo-eyebrow {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #8392ab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eo-card__body {
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eo-hero {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eo-title {
|
||||||
|
margin: 0.3rem 0 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #344767;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eo-subtitle {
|
||||||
|
margin: 0.35rem 0 0;
|
||||||
|
color: #67748e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eo-action {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
border: 1px solid rgba(94, 114, 228, 0.35);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: #fff;
|
||||||
|
color: #5e72e4;
|
||||||
|
padding: 0.65rem 0.9rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eo-highlights,
|
||||||
|
.eo-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eo-highlights {
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eo-grid--triple {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.eo-highlight,
|
||||||
|
.eo-list__item {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.85rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #eef2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eo-highlight__label,
|
||||||
|
.eo-list__label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #8392ab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eo-highlight__value,
|
||||||
|
.eo-list__value {
|
||||||
|
color: #344767;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eo-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.eo-highlights,
|
||||||
|
.eo-grid,
|
||||||
|
.eo-grid--triple {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,72 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card-body text-center">
|
<div class="epc card">
|
||||||
<!-- Employee Avatar -->
|
<div class="epc__cover"></div>
|
||||||
<div class="avatar avatar-xl position-relative">
|
|
||||||
|
<div class="epc__body">
|
||||||
|
<div class="epc__avatar-wrap">
|
||||||
<img
|
<img
|
||||||
v-if="avatarUrl"
|
v-if="avatarUrl"
|
||||||
:src="avatarUrl"
|
:src="avatarUrl"
|
||||||
:alt="employeeName"
|
:alt="employeeName"
|
||||||
class="w-100 border-radius-lg shadow-sm"
|
class="epc__avatar"
|
||||||
/>
|
/>
|
||||||
<div
|
<div v-else class="epc__avatar epc__avatar--fallback">
|
||||||
v-else
|
<span>{{ initials }}</span>
|
||||||
class="avatar avatar-xl rounded-circle bg-gradient-primary text-white border-radius-lg shadow-sm d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<span class="text-xl font-weight-bold">{{ initials }}</span>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href="javascript:;"
|
|
||||||
class="btn btn-sm btn-icon-only bg-gradient-primary position-absolute bottom-0 end-0 mb-n2 me-n2"
|
|
||||||
@click="$emit('edit-avatar')"
|
|
||||||
>
|
|
||||||
<i class="fa fa-pen"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Employee Name -->
|
<button type="button" class="epc__edit" @click="$emit('edit-avatar')">
|
||||||
<h5 class="font-weight-bolder mb-0">
|
<i class="fas fa-pen"></i>
|
||||||
{{ employeeName }}
|
</button>
|
||||||
</h5>
|
|
||||||
<p class="text-sm text-secondary mb-3">
|
|
||||||
{{ jobTitle }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Quick Stats -->
|
|
||||||
<div class="row text-center mt-3">
|
|
||||||
<div class="col-6 border-end">
|
|
||||||
<h6 class="text-sm font-weight-bolder mb-0">
|
|
||||||
{{ hireDate }}
|
|
||||||
</h6>
|
|
||||||
<p class="text-xs text-secondary mb-0">Date embauche</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<h6 class="text-sm font-weight-bolder mb-0">
|
|
||||||
<i
|
|
||||||
class="fas"
|
|
||||||
:class="
|
|
||||||
isActive
|
|
||||||
? 'fa-check-circle text-success'
|
|
||||||
: 'fa-times-circle text-danger'
|
|
||||||
"
|
|
||||||
></i>
|
|
||||||
</h6>
|
|
||||||
<p class="text-xs text-secondary mb-0">Statut</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Thanatopractitioner Badge -->
|
<div class="epc__identity">
|
||||||
<div v-if="isThanatopractitioner" class="mt-3">
|
<div class="epc__badges">
|
||||||
<span class="badge badge-sm bg-gradient-info">
|
<span class="epc__badge" :class="isActive ? 'epc__badge--success' : 'epc__badge--muted'">
|
||||||
<i class="fas fa-user-md me-1"></i>
|
{{ isActive ? "Actif" : "Inactif" }}
|
||||||
Thanatopractitioner
|
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="isThanatopractitioner" class="epc__badge epc__badge--info">
|
||||||
|
Thanatopracteur
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="epc__name">{{ employeeName }}</h1>
|
||||||
|
<p class="epc__role">{{ jobTitle || status }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="epc__meta">
|
||||||
|
<div class="epc__meta-item">
|
||||||
|
<span class="epc__meta-label">Embauche</span>
|
||||||
|
<strong>{{ hireDate }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="epc__meta-item">
|
||||||
|
<span class="epc__meta-label">Contact</span>
|
||||||
|
<strong>{{ employee.email || employee.phone || "Non renseigne" }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits } from "vue";
|
import { defineProps, defineEmits } from 'vue';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
avatarUrl: {
|
avatarUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -76,13 +58,17 @@ defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
employee: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
employeeName: {
|
employeeName: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
jobTitle: {
|
jobTitle: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "Employé",
|
default: "Employe",
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -106,35 +92,140 @@ defineEmits(["edit-avatar"]);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.avatar {
|
.epc {
|
||||||
width: 4rem;
|
overflow: hidden;
|
||||||
height: 4rem;
|
border: 1px solid rgba(131, 146, 171, 0.25);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-xl {
|
.epc__cover {
|
||||||
width: 6rem;
|
height: 84px;
|
||||||
height: 6rem;
|
background: linear-gradient(135deg, #f8f9ff 0%, #ffffff 100%);
|
||||||
|
border-bottom: 1px solid rgba(94, 114, 228, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-radius-lg {
|
.epc__body {
|
||||||
border-radius: 0.5rem;
|
padding: 0 1.25rem 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-gradient-primary {
|
.epc__avatar-wrap {
|
||||||
background: linear-gradient(310deg, #7928ca, #ff0080);
|
position: relative;
|
||||||
|
width: 84px;
|
||||||
|
height: 84px;
|
||||||
|
margin-top: -42px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-gradient-info {
|
.epc__avatar {
|
||||||
background: linear-gradient(310deg, #0dcaf0, #6bb9f0);
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 18px;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 4px solid #fff;
|
||||||
|
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadow-sm {
|
.epc__avatar--fallback {
|
||||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(310deg, #5e72e4 0%, #825ee4 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-icon-only {
|
.epc__edit {
|
||||||
width: 2.5rem;
|
position: absolute;
|
||||||
height: 2.5rem;
|
right: -6px;
|
||||||
padding: 0.625rem;
|
bottom: -6px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #fff;
|
||||||
|
color: #5e72e4;
|
||||||
|
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.epc__identity {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.epc__badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.epc__badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0.25rem 0.7rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.epc__badge--success {
|
||||||
|
background: #ecfdf3;
|
||||||
|
color: #047857;
|
||||||
|
}
|
||||||
|
|
||||||
|
.epc__badge--muted {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.epc__badge--info {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.epc__name {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #344767;
|
||||||
|
}
|
||||||
|
|
||||||
|
.epc__role {
|
||||||
|
margin: 0.35rem 0 0;
|
||||||
|
color: #8392ab;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.epc__meta {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.epc__meta-item {
|
||||||
|
padding: 0.85rem 0.95rem;
|
||||||
|
border: 1px solid #f1f5f9;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.epc__meta-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #8392ab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.epc__meta strong {
|
||||||
|
display: block;
|
||||||
|
color: #344767;
|
||||||
|
font-size: 0.92rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,43 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<ul class="nav nav-pills flex-column">
|
<nav class="ed-nav">
|
||||||
<TabNavigationItem
|
<button
|
||||||
icon="fas fa-eye"
|
v-for="tab in tabs"
|
||||||
label="Aperçu"
|
:key="tab.id"
|
||||||
:is-active="activeTab === 'overview'"
|
type="button"
|
||||||
spacing=""
|
class="ed-nav__item"
|
||||||
@click="$emit('change-tab', 'overview')"
|
:class="{ 'ed-nav__item--active': activeTab === tab.id }"
|
||||||
/>
|
@click="$emit('change-tab', tab.id)"
|
||||||
<TabNavigationItem
|
>
|
||||||
icon="fas fa-info-circle"
|
<span class="ed-nav__icon">
|
||||||
label="Informations"
|
<i :class="tab.icon"></i>
|
||||||
:is-active="activeTab === 'info'"
|
</span>
|
||||||
@click="$emit('change-tab', 'info')"
|
<span class="ed-nav__label">{{ tab.label }}</span>
|
||||||
/>
|
</button>
|
||||||
<TabNavigationItem
|
</nav>
|
||||||
icon="fas fa-file-alt"
|
|
||||||
label="Documents"
|
|
||||||
:is-active="activeTab === 'documents'"
|
|
||||||
@click="$emit('change-tab', 'documents')"
|
|
||||||
/>
|
|
||||||
<TabNavigationItem
|
|
||||||
v-if="isThanatopractitioner"
|
|
||||||
icon="fas fa-user-md"
|
|
||||||
label="Praticien"
|
|
||||||
:is-active="activeTab === 'practitioner'"
|
|
||||||
@click="$emit('change-tab', 'practitioner')"
|
|
||||||
/>
|
|
||||||
<TabNavigationItem
|
|
||||||
icon="fas fa-chart-line"
|
|
||||||
label="Activité"
|
|
||||||
:is-active="activeTab === 'activity'"
|
|
||||||
@click="$emit('change-tab', 'activity')"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import TabNavigationItem from "@/components/atoms/client/TabNavigationItem.vue";
|
import { computed, defineProps, defineEmits } from "vue";
|
||||||
import { defineProps, defineEmits } from "vue";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
activeTab: {
|
activeTab: {
|
||||||
@ -50,5 +30,80 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["change-tab"]);
|
defineEmits(["change-tab"]);
|
||||||
|
|
||||||
|
const tabs = computed(() => {
|
||||||
|
const items = [
|
||||||
|
{ id: "overview", label: "Apercu", icon: "fas fa-eye" },
|
||||||
|
{ id: "info", label: "Informations", icon: "fas fa-info-circle" },
|
||||||
|
{ id: "documents", label: "Documents", icon: "fas fa-file-alt" },
|
||||||
|
{ id: "activity", label: "Activite", icon: "fas fa-chart-line" },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (props.isThanatopractitioner) {
|
||||||
|
items.splice(3, 0, {
|
||||||
|
id: "practitioner",
|
||||||
|
label: "Praticien",
|
||||||
|
icon: "fas fa-user-md",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ed-nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ed-nav__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.8rem 0.95rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 0.85rem;
|
||||||
|
background: #fff;
|
||||||
|
color: #67748e;
|
||||||
|
transition: all 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ed-nav__item:hover {
|
||||||
|
border-color: rgba(94, 114, 228, 0.18);
|
||||||
|
background: #f8f9ff;
|
||||||
|
color: #344767;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ed-nav__item--active {
|
||||||
|
background: linear-gradient(135deg, rgba(94, 114, 228, 0.12), #ffffff);
|
||||||
|
border-color: rgba(94, 114, 228, 0.22);
|
||||||
|
color: #344767;
|
||||||
|
box-shadow: 0 10px 24px rgba(94, 114, 228, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ed-nav__icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 0.7rem;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #5e72e4;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ed-nav__item--active .ed-nav__icon {
|
||||||
|
background: linear-gradient(310deg, #5e72e4 0%, #825ee4 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ed-nav__label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -0,0 +1,275 @@
|
|||||||
|
<template>
|
||||||
|
<div class="eut">
|
||||||
|
<div class="eut-card">
|
||||||
|
<div class="eut-card__header">
|
||||||
|
<span class="eut-card__title">Compte utilisateur</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="eut-card__body">
|
||||||
|
<div v-if="employee.user" class="eut-linked">
|
||||||
|
<div class="eut-linked__item">
|
||||||
|
<span class="eut-label">Utilisateur lie</span>
|
||||||
|
<strong class="eut-value">{{ employee.user.name }}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="eut-linked__item">
|
||||||
|
<span class="eut-label">Email</span>
|
||||||
|
<strong class="eut-value">{{ employee.user.email }}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="eut-btn eut-btn--muted"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@click="detachUser"
|
||||||
|
>
|
||||||
|
Detacher l'utilisateur
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="showCreateForm" class="eut-create">
|
||||||
|
<div class="eut-create__header">Creer un utilisateur</div>
|
||||||
|
|
||||||
|
<div class="eut-grid">
|
||||||
|
<div>
|
||||||
|
<label class="eut-label" for="create-user-name">Nom</label>
|
||||||
|
<input
|
||||||
|
id="create-user-name"
|
||||||
|
v-model="createForm.name"
|
||||||
|
type="text"
|
||||||
|
class="eut-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="eut-label" for="create-user-email">Email</label>
|
||||||
|
<input
|
||||||
|
id="create-user-email"
|
||||||
|
v-model="createForm.email"
|
||||||
|
type="email"
|
||||||
|
class="eut-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="eut-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="eut-btn eut-btn--primary"
|
||||||
|
:disabled="isLoading || !createForm.name.trim() || !createForm.email.trim()"
|
||||||
|
@click="createAndAttachUser"
|
||||||
|
>
|
||||||
|
{{ isLoading ? "Creation..." : "Creer" }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="eut-btn eut-btn--muted"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@click="cancelCreate"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="eut-empty">
|
||||||
|
<p class="eut-empty__title">Aucun utilisateur lie</p>
|
||||||
|
<p class="eut-empty__sub">
|
||||||
|
Cet employe n'a pas encore de compte utilisateur associe.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="eut-btn eut-btn--primary"
|
||||||
|
@click="openCreate"
|
||||||
|
>
|
||||||
|
Creer un utilisateur
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref, watch, defineProps, defineEmits } from "vue";
|
||||||
|
import { useUserStore } from "@/stores/userStore";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
employee: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["employee-updated", "notify-success", "notify-error"]);
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const showCreateForm = ref(false);
|
||||||
|
const createForm = reactive({
|
||||||
|
name: props.employee.full_name || "",
|
||||||
|
email: props.employee.email || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLoading = computed(() => userStore.isLoading);
|
||||||
|
|
||||||
|
const resetCreateForm = () => {
|
||||||
|
createForm.name = props.employee.full_name || "";
|
||||||
|
createForm.email = props.employee.email || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.employee,
|
||||||
|
() => {
|
||||||
|
resetCreateForm();
|
||||||
|
if (props.employee.user) {
|
||||||
|
showCreateForm.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const openCreate = () => {
|
||||||
|
resetCreateForm();
|
||||||
|
showCreateForm.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelCreate = () => {
|
||||||
|
resetCreateForm();
|
||||||
|
showCreateForm.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const detachUser = async () => {
|
||||||
|
await emit("employee-updated", { user_id: null });
|
||||||
|
emit("notify-success", "Utilisateur detache");
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAndAttachUser = async () => {
|
||||||
|
try {
|
||||||
|
const user = await userStore.createUser({
|
||||||
|
name: createForm.name.trim(),
|
||||||
|
email: createForm.email.trim(),
|
||||||
|
password: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await emit("employee-updated", { user_id: user.id });
|
||||||
|
emit("notify-success", "Utilisateur attache");
|
||||||
|
showCreateForm.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error?.response?.data?.message ||
|
||||||
|
error?.response?.data?.error ||
|
||||||
|
(error?.response?.data?.errors
|
||||||
|
? Object.values(error.response.data.errors).flat()[0]
|
||||||
|
: null) ||
|
||||||
|
"Impossible de creer l'utilisateur";
|
||||||
|
|
||||||
|
emit("notify-error", message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.eut-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(131, 146, 171, 0.25);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eut-card__header {
|
||||||
|
padding: 0.875rem 1.25rem;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eut-card__title,
|
||||||
|
.eut-label,
|
||||||
|
.eut-create__header {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #8392ab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eut-card__body {
|
||||||
|
padding: 1.25rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eut-create,
|
||||||
|
.eut-linked,
|
||||||
|
.eut-empty {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eut-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eut-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eut-input {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #d2d6da;
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
padding: 0.75rem 0.9rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eut-btn {
|
||||||
|
border: 1px solid #111827;
|
||||||
|
background: #111827;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eut-btn--primary {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eut-btn--muted {
|
||||||
|
border-color: #e5e7eb;
|
||||||
|
background: #fff;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eut-linked__item {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.85rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #eef2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eut-empty__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #344767;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eut-empty__sub,
|
||||||
|
.eut-value {
|
||||||
|
margin: 0;
|
||||||
|
color: #67748e;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.eut-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -67,65 +67,56 @@ const tabs = [
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.tab-nav {
|
.tab-nav {
|
||||||
--brand: #4f46e5;
|
|
||||||
--brand-lt: #eef2ff;
|
|
||||||
--brand-dk: #3730a3;
|
|
||||||
--surface-2: #f8fafc;
|
|
||||||
--border-lt: #f1f5f9;
|
|
||||||
--text-1: #0f172a;
|
|
||||||
--text-2: #64748b;
|
|
||||||
--text-3: #94a3b8;
|
|
||||||
--r-sm: 8px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item {
|
.tab-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 9px;
|
gap: 0.5rem;
|
||||||
padding: 8px 11px;
|
padding: 0.65rem 0.9rem;
|
||||||
border-radius: var(--r-sm);
|
border-radius: 0.5rem;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 13.5px;
|
font-size: 0.875rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-2);
|
color: #67748e;
|
||||||
transition: background 0.12s, color 0.12s;
|
transition: all 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
.tab-item:hover {
|
.tab-item:hover {
|
||||||
background: var(--surface-2);
|
background-color: #f8f9fa;
|
||||||
color: var(--text-1);
|
|
||||||
}
|
}
|
||||||
.tab-item.active {
|
.tab-item.active {
|
||||||
background: var(--brand-lt);
|
background: linear-gradient(310deg, #7928ca, #ff0080);
|
||||||
color: var(--brand);
|
color: #fff;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.09),
|
||||||
|
0 2px 3px -1px rgba(0, 0, 0, 0.07);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-icon {
|
.tab-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
color: var(--text-3);
|
color: inherit;
|
||||||
}
|
}
|
||||||
.tab-item.active .tab-icon {
|
.tab-item.active .tab-icon {
|
||||||
color: var(--brand);
|
color: #fff;
|
||||||
}
|
}
|
||||||
.tab-label {
|
.tab-label {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-badge {
|
.tab-badge {
|
||||||
min-width: 19px;
|
min-width: 19px;
|
||||||
height: 19px;
|
height: 19px;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
border-radius: 10px;
|
border-radius: 999px;
|
||||||
background: var(--brand);
|
background: linear-gradient(310deg, #17ad37 0%, #98ec2d 100%);
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 10.5px;
|
font-size: 10.5px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@ -134,6 +125,6 @@ const tabs = [
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.tab-item.active .tab-badge {
|
.tab-item.active .tab-badge {
|
||||||
background: var(--brand-dk);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -30,6 +30,15 @@ export interface LoginResponse {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CheckEmailResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: {
|
||||||
|
user: User;
|
||||||
|
has_password: boolean;
|
||||||
|
};
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const AuthService = {
|
export const AuthService = {
|
||||||
async login(payload: LoginPayload): Promise<LoginResponse> {
|
async login(payload: LoginPayload): Promise<LoginResponse> {
|
||||||
const response = await request<LoginResponse>({
|
const response = await request<LoginResponse>({
|
||||||
@ -61,6 +70,40 @@ export const AuthService = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async checkEmail(email: string): Promise<CheckEmailResponse> {
|
||||||
|
return request<CheckEmailResponse>({
|
||||||
|
url: "/api/auth/check-email",
|
||||||
|
method: "post",
|
||||||
|
data: { email },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async createPassword(payload: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
password_confirmation: string;
|
||||||
|
}): Promise<LoginResponse> {
|
||||||
|
const response = await request<LoginResponse>({
|
||||||
|
url: "/api/auth/create-password",
|
||||||
|
method: "post",
|
||||||
|
data: payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success && response.data.token) {
|
||||||
|
localStorage.setItem("auth_token", response.data.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
applyLoginResponse(response: LoginResponse) {
|
||||||
|
if (response.success && response.data.token) {
|
||||||
|
localStorage.setItem("auth_token", response.data.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
async logout() {
|
async logout() {
|
||||||
try {
|
try {
|
||||||
await request<void>({ url: "/api/auth/logout", method: "post" });
|
await request<void>({ url: "/api/auth/logout", method: "post" });
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export interface EmployeeAddress {
|
|||||||
|
|
||||||
export interface Employee {
|
export interface Employee {
|
||||||
id: number;
|
id: number;
|
||||||
|
user_id?: number | null;
|
||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
full_name: string;
|
full_name: string;
|
||||||
@ -31,6 +32,11 @@ export interface Employee {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
} | null;
|
} | null;
|
||||||
|
user?: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmployeeListResponse {
|
export interface EmployeeListResponse {
|
||||||
@ -80,6 +86,7 @@ export interface CreateEmployeePayload {
|
|||||||
|
|
||||||
export interface UpdateEmployeePayload extends Partial<CreateEmployeePayload> {
|
export interface UpdateEmployeePayload extends Partial<CreateEmployeePayload> {
|
||||||
id: number;
|
id: number;
|
||||||
|
user_id?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmployeeService = {
|
export const EmployeeService = {
|
||||||
|
|||||||
65
thanasoft-front/src/services/user.ts
Normal file
65
thanasoft-front/src/services/user.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { request } from "./http";
|
||||||
|
|
||||||
|
export interface UserSummary {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateUserPayload {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
password?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateUserPayload {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
password?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserService = {
|
||||||
|
async searchUserByEmail(email: string): Promise<UserSummary | null> {
|
||||||
|
const response = await request<{
|
||||||
|
data: UserSummary | null;
|
||||||
|
message: string;
|
||||||
|
}>({
|
||||||
|
url: "/api/users",
|
||||||
|
method: "get",
|
||||||
|
params: { email },
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async createUser(payload: CreateUserPayload): Promise<UserSummary> {
|
||||||
|
const response = await request<{
|
||||||
|
data: UserSummary;
|
||||||
|
message: string;
|
||||||
|
}>({
|
||||||
|
url: "/api/users",
|
||||||
|
method: "post",
|
||||||
|
data: payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateUser(payload: UpdateUserPayload): Promise<UserSummary> {
|
||||||
|
const { id, ...data } = payload;
|
||||||
|
|
||||||
|
const response = await request<{
|
||||||
|
data: UserSummary;
|
||||||
|
message: string;
|
||||||
|
}>({
|
||||||
|
url: `/api/users/${id}`,
|
||||||
|
method: "put",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserService;
|
||||||
@ -1,5 +1,10 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import type { User, LoginPayload, RegisterPayload } from "@/services/auth";
|
import type {
|
||||||
|
User,
|
||||||
|
LoginPayload,
|
||||||
|
LoginResponse,
|
||||||
|
RegisterPayload,
|
||||||
|
} from "@/services/auth";
|
||||||
import AuthService from "@/services/auth";
|
import AuthService from "@/services/auth";
|
||||||
|
|
||||||
export const useAuthStore = defineStore("auth", {
|
export const useAuthStore = defineStore("auth", {
|
||||||
@ -49,6 +54,18 @@ export const useAuthStore = defineStore("auth", {
|
|||||||
throw new Error(response.message || "Login failed");
|
throw new Error(response.message || "Login failed");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
applyLoginResponse(response: LoginResponse) {
|
||||||
|
const applied = AuthService.applyLoginResponse(response);
|
||||||
|
|
||||||
|
if (applied.success && applied.data.user && applied.data.token) {
|
||||||
|
this.user = applied.data.user;
|
||||||
|
this.token = applied.data.token;
|
||||||
|
this.checked = true;
|
||||||
|
return this.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(applied.message || "Login failed");
|
||||||
|
},
|
||||||
async register(payload: RegisterPayload) {
|
async register(payload: RegisterPayload) {
|
||||||
const response = await AuthService.register(payload);
|
const response = await AuthService.register(payload);
|
||||||
|
|
||||||
|
|||||||
79
thanasoft-front/src/stores/userStore.ts
Normal file
79
thanasoft-front/src/stores/userStore.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import UserService from "@/services/user";
|
||||||
|
import type {
|
||||||
|
CreateUserPayload,
|
||||||
|
UpdateUserPayload,
|
||||||
|
UserSummary,
|
||||||
|
} from "@/services/user";
|
||||||
|
|
||||||
|
export const useUserStore = defineStore("user", () => {
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
const searchedUser = ref<UserSummary | null>(null);
|
||||||
|
|
||||||
|
const isLoading = computed(() => loading.value);
|
||||||
|
const currentSearchUser = computed(() => searchedUser.value);
|
||||||
|
|
||||||
|
const searchUserByEmail = async (email: string) => {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await UserService.searchUserByEmail(email);
|
||||||
|
searchedUser.value = user;
|
||||||
|
return user;
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value =
|
||||||
|
err.response?.data?.message || err.message || "Failed to search user";
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUser = async (payload: CreateUserPayload) => {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await UserService.createUser(payload);
|
||||||
|
searchedUser.value = user;
|
||||||
|
return user;
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value =
|
||||||
|
err.response?.data?.message || err.message || "Failed to create user";
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUser = async (payload: UpdateUserPayload) => {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await UserService.updateUser(payload);
|
||||||
|
searchedUser.value = user;
|
||||||
|
return user;
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value =
|
||||||
|
err.response?.data?.message || err.message || "Failed to update user";
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
searchedUser,
|
||||||
|
isLoading,
|
||||||
|
currentSearchUser,
|
||||||
|
searchUserByEmail,
|
||||||
|
createUser,
|
||||||
|
updateUser,
|
||||||
|
};
|
||||||
|
});
|
||||||
@ -5,13 +5,14 @@
|
|||||||
:is-loading="employeeStore.isLoading"
|
:is-loading="employeeStore.isLoading"
|
||||||
:employee-avatar="employeeAvatar"
|
:employee-avatar="employeeAvatar"
|
||||||
:active-tab="activeTab"
|
:active-tab="activeTab"
|
||||||
:file-input="fileInput"
|
|
||||||
:thanatopractitioner-data="thanatopractitionerData"
|
:thanatopractitioner-data="thanatopractitionerData"
|
||||||
:practitioner-documents="practitionerDocuments"
|
:practitioner-documents="practitionerDocuments"
|
||||||
@update-the-employee="updateEmployee"
|
@update-the-employee="updateEmployee"
|
||||||
@create-practitioner-document="createPractitionerDocument"
|
@create-practitioner-document="createPractitionerDocument"
|
||||||
@updating-practitioner-document="updatePractitionerDocument"
|
@updating-practitioner-document="updatePractitionerDocument"
|
||||||
@remove-practitioner-document="removePractitionerDocument"
|
@remove-practitioner-document="removePractitionerDocument"
|
||||||
|
@notify-success="handleSuccessMessage"
|
||||||
|
@notify-error="handleErrorMessage"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -30,7 +31,6 @@ const notificationStore = useNotificationStore();
|
|||||||
const employee_id = Number(route.params.id);
|
const employee_id = Number(route.params.id);
|
||||||
const activeTab = ref("overview");
|
const activeTab = ref("overview");
|
||||||
const employeeAvatar = ref(null);
|
const employeeAvatar = ref(null);
|
||||||
const fileInput = ref(null);
|
|
||||||
const thanatopractitionerData = ref(null);
|
const thanatopractitionerData = ref(null);
|
||||||
const practitionerDocuments = ref([]);
|
const practitionerDocuments = ref([]);
|
||||||
|
|
||||||
@ -104,4 +104,12 @@ const removePractitionerDocument = async (documentId) => {
|
|||||||
notificationStore.error("Erreur", "Impossible de supprimer le document");
|
notificationStore.error("Erreur", "Impossible de supprimer le document");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSuccessMessage = (message) => {
|
||||||
|
notificationStore.updated(message || "Employé");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleErrorMessage = (message) => {
|
||||||
|
notificationStore.error("Erreur", message || "Une erreur est survenue");
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -12,21 +12,23 @@
|
|||||||
<div class="mx-auto text-center col-lg-5">
|
<div class="mx-auto text-center col-lg-5">
|
||||||
<h1 class="mt-5 mb-2 text-white">Bonjour !</h1>
|
<h1 class="mt-5 mb-2 text-white">Bonjour !</h1>
|
||||||
<p class="text-white text-lead">
|
<p class="text-white text-lead">
|
||||||
Veuillez entrer vos identifiants pour vous connecter
|
Entrez votre email pour continuer
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row mt-lg-n10 mt-md-n11 mt-n10 justify-content-center">
|
<div class="row mt-lg-n10 mt-md-n11 mt-n10 justify-content-center">
|
||||||
<div class="mx-auto col-xl-4 col-lg-5 col-md-7">
|
<div class="mx-auto col-xl-4 col-lg-5 col-md-7">
|
||||||
<div class="card z-index-0">
|
<div class="card z-index-0">
|
||||||
<div class="pt-4 text-center card-header">
|
<div class="pt-4 text-center card-header">
|
||||||
<h5>Connectez-vous</h5>
|
<h5>{{ stepTitle }}</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form role="form" class="text-start" @submit.prevent="handleLogin">
|
<form role="form" class="text-start" @submit.prevent="handleSubmit">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<SoftInput
|
<SoftInput
|
||||||
id="email"
|
id="email"
|
||||||
@ -35,8 +37,11 @@
|
|||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
name="email"
|
name="email"
|
||||||
:is-required="true"
|
:is-required="true"
|
||||||
|
:disabled="currentStep !== 'email'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template v-if="currentStep === 'password'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<SoftInput
|
<SoftInput
|
||||||
id="password"
|
id="password"
|
||||||
@ -47,6 +52,7 @@
|
|||||||
:is-required="true"
|
:is-required="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SoftSwitch
|
<SoftSwitch
|
||||||
id="rememberMe"
|
id="rememberMe"
|
||||||
:checked="remember"
|
:checked="remember"
|
||||||
@ -55,6 +61,32 @@
|
|||||||
>
|
>
|
||||||
Souvenez de moi
|
Souvenez de moi
|
||||||
</SoftSwitch>
|
</SoftSwitch>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="currentStep === 'create-password'">
|
||||||
|
<div class="mb-3">
|
||||||
|
<SoftInput
|
||||||
|
id="new-password"
|
||||||
|
v-model="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Nouveau mot de passe"
|
||||||
|
:is-required="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<SoftInput
|
||||||
|
id="password-confirmation"
|
||||||
|
v-model="passwordConfirmation"
|
||||||
|
name="password_confirmation"
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirmer le mot de passe"
|
||||||
|
:is-required="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="errorMessage"
|
v-if="errorMessage"
|
||||||
class="alert alert-danger text-white"
|
class="alert alert-danger text-white"
|
||||||
@ -62,16 +94,30 @@
|
|||||||
>
|
>
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
|
||||||
|
<div class="text-center d-grid gap-2">
|
||||||
<SoftButton
|
<SoftButton
|
||||||
type="submit"
|
type="submit"
|
||||||
class="my-4 mb-2"
|
class="my-2 mb-0"
|
||||||
variant="gradient"
|
variant="gradient"
|
||||||
color="info"
|
color="info"
|
||||||
full-width
|
full-width
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
>
|
>
|
||||||
{{ isLoading ? "Connexion..." : "Se connecter" }}
|
{{ submitLabel }}
|
||||||
|
</SoftButton>
|
||||||
|
|
||||||
|
<SoftButton
|
||||||
|
v-if="currentStep !== 'email'"
|
||||||
|
type="button"
|
||||||
|
class="mt-0"
|
||||||
|
variant="outline"
|
||||||
|
color="secondary"
|
||||||
|
full-width
|
||||||
|
:disabled="isLoading"
|
||||||
|
@click="goBack"
|
||||||
|
>
|
||||||
|
Retour
|
||||||
</SoftButton>
|
</SoftButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -81,47 +127,134 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import AuthService from "@/services/auth";
|
||||||
import SoftInput from "@/components/SoftInput.vue";
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
import SoftSwitch from "@/components/SoftSwitch.vue";
|
import SoftSwitch from "@/components/SoftSwitch.vue";
|
||||||
import SoftButton from "@/components/SoftButton.vue";
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
|
||||||
|
type Step = "email" | "password" | "create-password";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
const currentStep = ref<Step>("email");
|
||||||
|
const checkedEmail = ref("");
|
||||||
const email = ref("");
|
const email = ref("");
|
||||||
const password = ref("");
|
const password = ref("");
|
||||||
|
const passwordConfirmation = ref("");
|
||||||
const remember = ref(false);
|
const remember = ref(false);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const errorMessage = ref("");
|
const errorMessage = ref("");
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const stepTitle = computed(() => {
|
||||||
if (!email.value || !password.value) {
|
if (currentStep.value === "password") return "Saisissez votre mot de passe";
|
||||||
errorMessage.value = "Veuillez remplir tous les champs";
|
if (currentStep.value === "create-password") return "Creez votre mot de passe";
|
||||||
|
return "Connectez-vous";
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitLabel = computed(() => {
|
||||||
|
if (isLoading.value) {
|
||||||
|
if (currentStep.value === "email") return "Verification...";
|
||||||
|
if (currentStep.value === "create-password") return "Creation...";
|
||||||
|
return "Connexion...";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStep.value === "email") return "Suivant";
|
||||||
|
if (currentStep.value === "create-password") return "Creer et se connecter";
|
||||||
|
return "Se connecter";
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetPasswords = () => {
|
||||||
|
password.value = "";
|
||||||
|
passwordConfirmation.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
currentStep.value = "email";
|
||||||
|
errorMessage.value = "";
|
||||||
|
resetPasswords();
|
||||||
|
email.value = checkedEmail.value || email.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkEmailStep = async () => {
|
||||||
|
if (!email.value) {
|
||||||
|
errorMessage.value = "Veuillez entrer votre email";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading.value = true;
|
const response = await AuthService.checkEmail(email.value);
|
||||||
|
checkedEmail.value = email.value;
|
||||||
|
currentStep.value = response.data.has_password ? "password" : "create-password";
|
||||||
errorMessage.value = "";
|
errorMessage.value = "";
|
||||||
|
resetPasswords();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginStep = async () => {
|
||||||
|
if (!password.value) {
|
||||||
|
errorMessage.value = "Veuillez entrer votre mot de passe";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
await authStore.login({
|
await authStore.login({
|
||||||
email: email.value,
|
email: email.value,
|
||||||
password: password.value,
|
password: password.value,
|
||||||
remember: remember.value,
|
remember: remember.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Redirect to dashboard on success
|
|
||||||
router.push("/dashboards/dashboard-default");
|
router.push("/dashboards/dashboard-default");
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPasswordStep = async () => {
|
||||||
|
if (!password.value || !passwordConfirmation.value) {
|
||||||
|
errorMessage.value = "Veuillez remplir les deux champs de mot de passe";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.value !== passwordConfirmation.value) {
|
||||||
|
errorMessage.value = "Les mots de passe ne correspondent pas";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await AuthService.createPassword({
|
||||||
|
email: email.value,
|
||||||
|
password: password.value,
|
||||||
|
password_confirmation: passwordConfirmation.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
authStore.applyLoginResponse(response);
|
||||||
|
router.push("/dashboards/dashboard-default");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
isLoading.value = true;
|
||||||
|
errorMessage.value = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (currentStep.value === "email") {
|
||||||
|
await checkEmailStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStep.value === "password") {
|
||||||
|
await loginStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await createPasswordStep();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Login error:", error);
|
|
||||||
errorMessage.value =
|
errorMessage.value =
|
||||||
error.response?.data?.message ||
|
error.response?.data?.message ||
|
||||||
|
error.response?.data?.error ||
|
||||||
|
(error.response?.data?.data
|
||||||
|
? Object.values(error.response.data.data).flat()[0]
|
||||||
|
: null) ||
|
||||||
error.message ||
|
error.message ||
|
||||||
"Email ou mot de passe incorrect";
|
"Une erreur est survenue";
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -342,13 +342,13 @@
|
|||||||
"affectsGlobalScope": false
|
"affectsGlobalScope": false
|
||||||
},
|
},
|
||||||
"./src/services/auth.ts": {
|
"./src/services/auth.ts": {
|
||||||
"version": "16ebba3a31a188cd83e24cbf6f51059b11e9bae45b70d74cb846ef98b180bc83",
|
"version": "2eeaa10e80a0e2b6e8b2132d04bfd5807f406410e1937e96d835c0da3869a3c0",
|
||||||
"signature": "28ae1b4b650b0b5a33be34a437b12709e6720f5b793cfe4989c6de4d9712f1be",
|
"signature": "9fe51dbe2f2d4cf03c23b09324a28a1c4803ec8b1243dc934eb66b7e3911b141",
|
||||||
"affectsGlobalScope": false
|
"affectsGlobalScope": false
|
||||||
},
|
},
|
||||||
"./src/stores/auth.ts": {
|
"./src/stores/auth.ts": {
|
||||||
"version": "54a7256147ecea348010e7838bd11cfad8bbe0e1088ff33157270cf49b83d2df",
|
"version": "51e85d745280cf0053821aeab38ee8784d536c0a9011d7406cebcaec02466421",
|
||||||
"signature": "e372e4cc9e9ec057aec7bf27009c69b9a61f7496adb6982537fcefe8878c29cf",
|
"signature": "4cbf57bf88491cd2a06ecf58fa813172f1b1a9055fdd2891820cc85c5a357b03",
|
||||||
"affectsGlobalScope": false
|
"affectsGlobalScope": false
|
||||||
},
|
},
|
||||||
"./src/plugins/pinia.ts": {
|
"./src/plugins/pinia.ts": {
|
||||||
@ -447,8 +447,8 @@
|
|||||||
"affectsGlobalScope": false
|
"affectsGlobalScope": false
|
||||||
},
|
},
|
||||||
"./src/services/employee.ts": {
|
"./src/services/employee.ts": {
|
||||||
"version": "4f17ff95c56e77592e539121d1ae7da2acecb5f2bb19e2cdae453190e08f163a",
|
"version": "ac861e6512abfd47d9ff77021c1ca31c1fc875ee628e9b393c0337016d8d0fd9",
|
||||||
"signature": "fec2accc984083793b74bbd8a19ff8450a3408ebb2233ae639757320a8555174",
|
"signature": "2143dd5efb96ee5404f670294fd7449c1711ed38e586f8f0699dd25fab6c500f",
|
||||||
"affectsGlobalScope": false
|
"affectsGlobalScope": false
|
||||||
},
|
},
|
||||||
"./src/services/file.ts": {
|
"./src/services/file.ts": {
|
||||||
@ -541,6 +541,11 @@
|
|||||||
"signature": "e21d680d6f67c6ea626a7c5b4cf2261ac7c4aad308edb8b97b67b158a3f721b9",
|
"signature": "e21d680d6f67c6ea626a7c5b4cf2261ac7c4aad308edb8b97b67b158a3f721b9",
|
||||||
"affectsGlobalScope": false
|
"affectsGlobalScope": false
|
||||||
},
|
},
|
||||||
|
"./src/services/user.ts": {
|
||||||
|
"version": "f7649eeaf793a7004ce2a5c4617372d5a4cb962af933709dedbeed63c9b2ea00",
|
||||||
|
"signature": "c12a954ed184461f9d4d1c629838212d3656038c2ab0fb45e0b37b05649cf9b1",
|
||||||
|
"affectsGlobalScope": false
|
||||||
|
},
|
||||||
"./src/services/warehouse.ts": {
|
"./src/services/warehouse.ts": {
|
||||||
"version": "937e7069ca212188f8754cf7ae1c6d136ca78a758b1a106e12d25892ef2325bb",
|
"version": "937e7069ca212188f8754cf7ae1c6d136ca78a758b1a106e12d25892ef2325bb",
|
||||||
"signature": "e3d801bce51f50e67b236f77f0f227d6b157a4b551655bfb92f70239547c85b6",
|
"signature": "e3d801bce51f50e67b236f77f0f227d6b157a4b551655bfb92f70239547c85b6",
|
||||||
@ -593,7 +598,7 @@
|
|||||||
},
|
},
|
||||||
"./src/stores/employeeStore.ts": {
|
"./src/stores/employeeStore.ts": {
|
||||||
"version": "dc347efda269601d49e43b247f69739c88174a328b1fa6a0c2f23fcaa0b84085",
|
"version": "dc347efda269601d49e43b247f69739c88174a328b1fa6a0c2f23fcaa0b84085",
|
||||||
"signature": "ec41938d36dc9cb76375b8614f99c5734c0ebe0560bd597c4eaa2a509b50504c",
|
"signature": "9f15b71f8e81a981254013d8436a3a679fe7a1f7d590628f775e103b8573a52f",
|
||||||
"affectsGlobalScope": false
|
"affectsGlobalScope": false
|
||||||
},
|
},
|
||||||
"./src/stores/factureFournisseurStore.ts": {
|
"./src/stores/factureFournisseurStore.ts": {
|
||||||
@ -666,6 +671,11 @@
|
|||||||
"signature": "e166686f15adbdbc03e40ba1da69613e706f1d672d790fc7ac0a081490f859c3",
|
"signature": "e166686f15adbdbc03e40ba1da69613e706f1d672d790fc7ac0a081490f859c3",
|
||||||
"affectsGlobalScope": false
|
"affectsGlobalScope": false
|
||||||
},
|
},
|
||||||
|
"./src/stores/userStore.ts": {
|
||||||
|
"version": "78156d36cb4fbfc87d10010e3984a88489a3de7e4f50df674aaca6aca9bbce8f",
|
||||||
|
"signature": "6ca38694a86319822f6d998d9b027ea3f68310a518964d9d98d5dc29c9696fd7",
|
||||||
|
"affectsGlobalScope": false
|
||||||
|
},
|
||||||
"./src/stores/warehouseStore.ts": {
|
"./src/stores/warehouseStore.ts": {
|
||||||
"version": "eb8a88b09c39a9ec6edb001cbca3399f4f15267e8f2cea71b3e9f928db8b1656",
|
"version": "eb8a88b09c39a9ec6edb001cbca3399f4f15267e8f2cea71b3e9f928db8b1656",
|
||||||
"signature": "df6be19856c6c2efb03600d31d00ae8650ccca92ba971a40f020c86801ca6b1d",
|
"signature": "df6be19856c6c2efb03600d31d00ae8650ccca92ba971a40f020c86801ca6b1d",
|
||||||
@ -936,6 +946,10 @@
|
|||||||
"./node_modules/tslib/tslib.d.ts",
|
"./node_modules/tslib/tslib.d.ts",
|
||||||
"./src/services/http.ts"
|
"./src/services/http.ts"
|
||||||
],
|
],
|
||||||
|
"./src/services/user.ts": [
|
||||||
|
"./node_modules/tslib/tslib.d.ts",
|
||||||
|
"./src/services/http.ts"
|
||||||
|
],
|
||||||
"./src/services/warehouse.ts": [
|
"./src/services/warehouse.ts": [
|
||||||
"./node_modules/tslib/tslib.d.ts",
|
"./node_modules/tslib/tslib.d.ts",
|
||||||
"./src/services/http.ts"
|
"./src/services/http.ts"
|
||||||
@ -1103,6 +1117,12 @@
|
|||||||
"./src/services/thanatopractitioner.ts",
|
"./src/services/thanatopractitioner.ts",
|
||||||
"./src/stores/notification.ts"
|
"./src/stores/notification.ts"
|
||||||
],
|
],
|
||||||
|
"./src/stores/userStore.ts": [
|
||||||
|
"./node_modules/pinia/dist/pinia.d.ts",
|
||||||
|
"./node_modules/tslib/tslib.d.ts",
|
||||||
|
"./node_modules/vue/dist/vue.d.ts",
|
||||||
|
"./src/services/user.ts"
|
||||||
|
],
|
||||||
"./src/stores/warehouseStore.ts": [
|
"./src/stores/warehouseStore.ts": [
|
||||||
"./node_modules/pinia/dist/pinia.d.ts",
|
"./node_modules/pinia/dist/pinia.d.ts",
|
||||||
"./node_modules/tslib/tslib.d.ts",
|
"./node_modules/tslib/tslib.d.ts",
|
||||||
@ -1334,6 +1354,11 @@
|
|||||||
"./node_modules/vue/dist/vue.d.ts",
|
"./node_modules/vue/dist/vue.d.ts",
|
||||||
"./src/services/thanatopractitioner.ts"
|
"./src/services/thanatopractitioner.ts"
|
||||||
],
|
],
|
||||||
|
"./src/stores/userStore.ts": [
|
||||||
|
"./node_modules/pinia/dist/pinia.d.ts",
|
||||||
|
"./node_modules/vue/dist/vue.d.ts",
|
||||||
|
"./src/services/user.ts"
|
||||||
|
],
|
||||||
"./src/stores/warehouseStore.ts": [
|
"./src/stores/warehouseStore.ts": [
|
||||||
"./node_modules/pinia/dist/pinia.d.ts",
|
"./node_modules/pinia/dist/pinia.d.ts",
|
||||||
"./src/services/warehouse.ts"
|
"./src/services/warehouse.ts"
|
||||||
@ -1444,6 +1469,7 @@
|
|||||||
"./src/services/supplierInvoice.ts",
|
"./src/services/supplierInvoice.ts",
|
||||||
"./src/services/thanatopractitioner.ts",
|
"./src/services/thanatopractitioner.ts",
|
||||||
"./src/services/tvaRate.ts",
|
"./src/services/tvaRate.ts",
|
||||||
|
"./src/services/user.ts",
|
||||||
"./src/services/warehouse.ts",
|
"./src/services/warehouse.ts",
|
||||||
"./src/shims-vue.d.ts",
|
"./src/shims-vue.d.ts",
|
||||||
"./src/soft-ui-dashboard.js",
|
"./src/soft-ui-dashboard.js",
|
||||||
@ -1630,6 +1656,7 @@
|
|||||||
"./src/stores/stockStore.ts",
|
"./src/stores/stockStore.ts",
|
||||||
"./src/stores/supplierInvoiceStore.ts",
|
"./src/stores/supplierInvoiceStore.ts",
|
||||||
"./src/stores/thanatopractitionerStore.ts",
|
"./src/stores/thanatopractitionerStore.ts",
|
||||||
|
"./src/stores/userStore.ts",
|
||||||
"./src/stores/warehouseStore.ts",
|
"./src/stores/warehouseStore.ts",
|
||||||
"./src/types/intervention.ts"
|
"./src/types/intervention.ts"
|
||||||
],
|
],
|
||||||
@ -1898,6 +1925,10 @@
|
|||||||
"./src/services/tvaRate.ts",
|
"./src/services/tvaRate.ts",
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"./src/services/user.ts",
|
||||||
|
1
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"./src/services/warehouse.ts",
|
"./src/services/warehouse.ts",
|
||||||
1
|
1
|
||||||
@ -2018,6 +2049,10 @@
|
|||||||
"./src/stores/thanatopractitionerStore.ts",
|
"./src/stores/thanatopractitionerStore.ts",
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"./src/stores/userStore.ts",
|
||||||
|
1
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"./src/stores/warehouseStore.ts",
|
"./src/stores/warehouseStore.ts",
|
||||||
1
|
1
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user