diff --git a/app/Events/TimelineMessage.php b/app/Events/TimelineMessage.php new file mode 100644 index 0000000..0c9328f --- /dev/null +++ b/app/Events/TimelineMessage.php @@ -0,0 +1,24 @@ +user = $user; + $this->message = $message; + $this->origin_client_id = $user->client_id; + $this->subject_type = $subject_type; + $this->subject_id = $subject_id; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index db903e8..cc900ff 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -20,7 +20,7 @@ class RegisteredUserController extends Controller */ public function create(): Response { - return Inertia::render('auth/register'); + return Inertia::render('Auth/Register'); } /** @@ -31,13 +31,17 @@ public function create(): Response public function store(Request $request): RedirectResponse { $request->validate([ - 'name' => 'required|string|max:255', - 'email' => 'required|string|lowercase|email|max:255|unique:'.User::class, + 'fname' => 'required|string|max:255', + 'mname' => 'nullable|string|max:255', + 'lname' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:'.User::class, 'password' => ['required', 'confirmed', Rules\Password::defaults()], ]); $user = User::create([ - 'name' => $request->name, + 'fname' => $request->fname, + 'mname' => $request->mname, + 'lname' => $request->lname, 'email' => $request->email, 'password' => Hash::make($request->password), ]); diff --git a/app/Http/Controllers/ClientAdmin/AdminDashboardController.php b/app/Http/Controllers/ClientAdmin/AdminDashboardController.php new file mode 100644 index 0000000..4718ea7 --- /dev/null +++ b/app/Http/Controllers/ClientAdmin/AdminDashboardController.php @@ -0,0 +1,67 @@ +user()->client->id; + + // Get all tasks with their users + $tasks = Task::where('client_id', $clientId) + ->with(['users' => function ($query) use ($clientId) { + $query->where('users.client_id', $clientId) + ->select('users.id', 'users.fname', 'users.mname', 'users.lname', 'users.email'); + }]) + ->get(); + + // Get task completion statistics + $taskStats = [ + 'total' => $tasks->count(), + 'completed' => $tasks->where('status', 'completed')->count(), + 'in_progress' => $tasks->where('status', 'in_progress')->count(), + 'pending' => $tasks->where('status', 'pending')->count(), + ]; + + // Get upcoming tasks (due in the next 7 days) + $upcomingTasks = $tasks->filter(function ($task) { + return $task->due_date && + Carbon::parse($task->due_date)->isBetween( + now(), + now()->addDays(7) + ); + })->values()->toArray(); + + // Get all users for the timeline + $users = User::where('client_id', $clientId) + ->select('id', 'fname', 'mname', 'lname', 'email') + ->get(); + + // Get recent timeline activities + $recentActivities = Timeline::where('origin_client_id', $clientId) + ->with(['user:id,fname,mname,lname,email']) + ->latest() + ->take(10) + ->get(); + + return Inertia::render('admin/admin-dashboard', [ + 'taskStats' => $taskStats, + 'upcomingTasks' => $upcomingTasks, + 'users' => $users, + 'tasks' => $tasks, + 'recentActivities' => $recentActivities + ]); + } +} diff --git a/app/Http/Controllers/Crud/ClientAdmin/MeetingController.php b/app/Http/Controllers/Crud/ClientAdmin/MeetingController.php new file mode 100644 index 0000000..10d4b5c --- /dev/null +++ b/app/Http/Controllers/Crud/ClientAdmin/MeetingController.php @@ -0,0 +1,182 @@ +user()->client->id; + + $meetings = Meeting::where('client_id', $clientId) + ->with(['users' => function ($query) use ($clientId) { + $query->where('users.client_id', $clientId) + ->select('users.id', 'users.fname', 'users.mname', 'users.lname', 'users.email'); + }]) + ->with(['tasks' => function ($query) use ($clientId) { + $query->where('tasks.client_id', $clientId) + ->select('tasks.id', 'tasks.name', 'tasks.status') + ->withPivot('meeting_id'); + }]) + ->get(); + + $availableUsers = User::where('client_id', $clientId) + ->select('id', 'fname', 'mname', 'lname', 'email') + ->get(); + + $availableTasks = Task::where('client_id', $clientId) + ->select('id', 'name', 'status') + ->get(); + + return Inertia::render('admin/admin-meeting-list', [ + 'meetings' => $meetings, + 'users' => $availableUsers, + 'tasks' => $availableTasks + ]); + } + + public function store(Request $request) + { + $request->validate([ + 'title' => 'required|string|max:255', + 'date' => 'required|date', + 'type' => 'required|string|max:255', + 'agenda_text' => 'required|string', + 'users' => 'nullable|array', + 'users.*' => 'exists:users,id', + 'tasks' => 'nullable|array', + 'tasks.*' => 'exists:tasks,id' + ]); + + $clientId = auth()->user()->client->id; + + // Create agenda first + $agenda = Agenda::create([ + 'text' => $request->agenda_text, + 'client_id' => $clientId, + ]); + + $meeting = Meeting::create([ + 'title' => $request->title, + 'date' => $request->date, + 'type' => $request->type, + 'agenda_id' => $agenda->id, + 'client_id' => $clientId, + ]); + + if ($request->has('users')) { + $meeting->users()->attach($request->users); + } + + if ($request->has('tasks')) { + $meeting->tasks()->attach($request->tasks); + } + + session()->flash('success', 'Meeting created successfully!'); + return back(); + } + + public function show(Meeting $meeting) + { + $clientId = auth()->user()->client->id; + + // Ensure the meeting belongs to the client + if ($meeting->client_id !== $clientId) { + abort(403); + } + + // Load the meeting with its relationships and agenda + $meeting->load(['users' => function ($query) use ($clientId) { + $query->where('users.client_id', $clientId) + ->select('users.id', 'users.fname', 'users.mname', 'users.lname', 'users.email'); + }]) + ->load(['tasks' => function ($query) use ($clientId) { + $query->where('tasks.client_id', $clientId) + ->select('tasks.id', 'tasks.name', 'tasks.status'); + }]) + ->load('agenda'); + + // Add agenda_text to the meeting data + $meeting->agenda_text = $meeting->agenda->text; + + // Get all available users and tasks for the form + $availableUsers = User::where('client_id', $clientId) + ->select('id', 'fname', 'mname', 'lname', 'email') + ->get(); + + $availableTasks = Task::where('client_id', $clientId) + ->select('id', 'name', 'status') + ->get(); + + return Inertia::render('admin/admin-meeting-details', [ + 'meeting' => $meeting, + 'users' => $availableUsers, + 'tasks' => $availableTasks + ]); + } + + public function update(Request $request, Meeting $meeting) + { + // Ensure the meeting belongs to the client + if ($meeting->client_id !== auth()->user()->client->id) { + abort(403, 'Unauthorized'); + } + + $request->validate([ + 'title' => 'required|string|max:255', + 'date' => 'required|date', + 'type' => 'required|string|max:255', + 'agenda_text' => 'required|string', + 'users' => 'nullable|array', + 'users.*' => 'exists:users,id', + 'tasks' => 'nullable|array', + 'tasks.*' => 'exists:tasks,id' + ]); + + // Update the meeting + $meeting->update([ + 'title' => $request->title, + 'date' => $request->date, + 'type' => $request->type, + ]); + + // Update the agenda + $meeting->agenda()->update([ + 'text' => $request->agenda_text + ]); + + // Sync users and tasks if they are provided + if ($request->has('users')) { + $meeting->users()->sync($request->users); + } + + if ($request->has('tasks')) { + $meeting->tasks()->sync($request->tasks); + } + + session()->flash('success', 'Meeting updated successfully!'); + return back(); + } + + public function destroy(Meeting $meeting) + { + // Ensure the meeting belongs to the client + if ($meeting->client_id !== auth()->user()->client->id) { + abort(403, 'Unauthorized'); + } + + // Delete the meeting (this will cascade delete the agenda due to the relationship) + $meeting->delete(); + + session()->flash('success', 'Meeting deleted successfully!'); + return redirect()->route('admin.meetings.index'); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Crud/ClientAdmin/TaskController.php b/app/Http/Controllers/Crud/ClientAdmin/TaskController.php new file mode 100644 index 0000000..05b22be --- /dev/null +++ b/app/Http/Controllers/Crud/ClientAdmin/TaskController.php @@ -0,0 +1,142 @@ +user()->client->id; + + $tasks = Task::where('client_id', $clientId) + ->with(['users' => function ($query) use ($clientId) { + $query->where('users.client_id', $clientId) + ->select('users.id', 'users.fname', 'users.mname', 'users.lname', 'users.email'); + }]) + ->get(); + + $users = User::where('client_id', $clientId) + ->select('id', 'fname', 'mname', 'lname', 'email') + ->get(); + + return Inertia::render('admin/admin-task-list', [ + 'tasks' => $tasks, + 'users' => $users + ]); + } + + public function store(Request $request) + { + $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'required|string|max:255', + 'status' => 'required|in:pending,in_progress,completed', + 'priority' => 'required|in:low,medium,high', + 'due_date' => 'nullable|date', + 'users' => 'array', + 'users.*' => 'exists:users,id', + 'meeting_id' => 'nullable|exists:meetings,id' + ]); + + $task = Task::create([ + 'name' => $request->name, + 'description' => $request->description, + 'status' => $request->status, + 'priority' => $request->priority, + 'due_date' => $request->due_date, + 'client_id' => auth()->user()->client->id + ]); + + if ($request->has('users')) { + $task->users()->sync($request->users); + } + + if ($request->has('meeting_id')) { + $task->meetings()->sync([$request->meeting_id]); + } + + $timelineMessageService = new TimelineMessageService(); + $timelineMessageService->taskCreated(auth()->user(), $task->name, 'Task', $task->id); + + session()->flash('success', 'Task created successfully!'); + return back(); + } + + public function show(Task $task, Request $request) + { + // Ensure the task belongs to the user's client + if ($task->client_id !== auth()->user()->client->id) { + abort(403, 'Unauthorized'); + } + + $clientId = auth()->user()->client->id; + + // Get all users for the client, for the task details page (to assign users to the task) + $all_users = User::where('client_id', $clientId) + ->select('id', 'fname', 'mname', 'lname', 'email') + ->get(); + + return Inertia::render('admin/admin-task-details', [ + 'task' => $task->load(['users' => function ($query) use ($clientId) { + $query->where('users.client_id', $clientId) + ->select('users.id', 'users.fname', 'users.mname', 'users.lname', 'users.email'); + }]), + 'all_users' => $all_users, + 'from_meeting' => $request->query('from_meeting') + ]); + } + + public function update(Request $request, Task $task) + { + // Ensure the task belongs to the user's client + if ($task->client_id !== auth()->user()->client->id) { + abort(403, 'Unauthorized'); + } + + $validatedData = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'required|string|max:255', + 'status' => 'required|in:pending,in_progress,completed', + 'priority' => 'required|in:low,medium,high', + 'due_date' => 'nullable|date', + 'users' => 'array', + 'users.*' => 'exists:users,id' + ]); + + $originalValues = $task->getOriginal(); + + + $task->update($request->only(['name', 'description', 'status', 'priority', 'due_date'])); + + if ($request->has('users')) { + $task->users()->sync($request->users); + } + + $timelineMessageService = new TimelineMessageService(); + $timelineMessageService->taskUpdated(auth()->user(), $task->name, 'Task', $task->id , $originalValues, $validatedData); + + session()->flash('success', 'Task updated successfully!'); + return back(); + } + + public function destroy(Task $task) + { + // Ensure the task belongs to the user's client + if ($task->client_id !== auth()->user()->client->id) { + abort(403, 'Unauthorized'); + } + + $task->delete(); + session()->flash('success', 'Task deleted successfully!'); + return redirect()->route('admin.tasks.index'); + } + + + +} \ No newline at end of file diff --git a/app/Http/Controllers/Crud/ClientAdmin/UserController.php b/app/Http/Controllers/Crud/ClientAdmin/UserController.php index 02c4c74..81a7e53 100644 --- a/app/Http/Controllers/Crud/ClientAdmin/UserController.php +++ b/app/Http/Controllers/Crud/ClientAdmin/UserController.php @@ -15,8 +15,10 @@ class UserController extends Controller { public function index() { - $users = User::where('client_id', auth()->user()->client_id)->get(); - return Inertia::render('Users/user-list', [ + $users = User::where('client_id', auth()->user()->client_id) + ->with('role:id,name') + ->get(); + return Inertia::render('admin/admin-user-list', [ 'users' => $users, 'roles' => Role::all(['id', 'name'])->except(1)//exclude the system admin role ]); @@ -25,7 +27,9 @@ public function index() public function store(Request $request) { $request->validate([ - 'name' => 'required|string|max:255', + 'fname' => 'required|string|max:255', + 'mname' => 'nullable|string|max:255', + 'lname' => 'required|string|max:255', 'email' => 'required|string|email|max:255|unique:users', 'password' => 'required|string|min:8|confirmed', 'client_id' => 'required|integer', @@ -37,13 +41,15 @@ public function store(Request $request) } User::create([ - 'name' => $request->name, + 'fname' => $request->fname, + 'mname' => $request->mname, + 'lname' => $request->lname, 'email' => $request->email, 'password' => Hash::make($request->password), 'client_id' => $request->client_id, 'role_id' => $request->role_id, ]); - + session()->flash('success', 'User created successfully!'); return back(); } @@ -54,7 +60,9 @@ public function update(Request $request, User $user) } $validated = $request->validate([ - 'name' => 'required|string|max:255', + 'fname' => 'required|string|max:255', + 'mname' => 'nullable|string|max:255', + 'lname' => 'required|string|max:255', 'email' => [ 'required', 'string', @@ -64,26 +72,28 @@ public function update(Request $request, User $user) ], ]); - $user->update($validated); + session()->flash('success', 'User updated successfully!'); return back(); } - public function show(User $user) + public function show(User $user, Request $request) { if ($user->client_id !== auth()->user()->client_id) { abort(403, 'Unauthorized'); } - return Inertia::render('Users/user-details', [ - 'user' => $user + return Inertia::render('admin/admin-user-details', [ + 'user' => $user, + 'from_meeting' => $request->query('from_meeting') ]); } public function destroy(User $user) { $user->delete(); - return redirect()->route('users.index'); + session()->flash('success', 'User deleted successfully!'); + return redirect()->route('admin.users.index'); } diff --git a/app/Http/Controllers/Crud/ClientController.php b/app/Http/Controllers/Crud/ClientController.php deleted file mode 100644 index f6856f1..0000000 --- a/app/Http/Controllers/Crud/ClientController.php +++ /dev/null @@ -1,66 +0,0 @@ -user()->client_id) + ->with(['users', 'tasks']) + ->when($request->input('view') === 'upcoming', function ($query) { + return $query->where('date', '>=', now()); + }) + ->orderBy('date', 'desc') + ->get(); + + return Inertia::render('member/member-meeting-list', [ + 'meetings' => $meetings, + 'users' => auth()->user()->client->users, + 'tasks' => auth()->user()->client->tasks, + ]); + } + + public function show(Meeting $meeting) + { + $meeting->load(['users', 'tasks']); + + return Inertia::render('member/member-meeting-details', [ + 'meeting' => $meeting, + 'users' => auth()->user()->client->users, + 'tasks' => auth()->user()->client->tasks, + ]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Crud/Member/MemberTaskController.php b/app/Http/Controllers/Crud/Member/MemberTaskController.php new file mode 100644 index 0000000..d678d30 --- /dev/null +++ b/app/Http/Controllers/Crud/Member/MemberTaskController.php @@ -0,0 +1,125 @@ +id(); + $clientId = auth()->user()->client->id; + + // Base query for tasks + $query = Task::where('client_id', $clientId) + ->with(['users' => function ($query) use ($clientId) { + $query->where('users.client_id', $clientId) + ->select('users.id', 'users.fname', 'users.mname', 'users.lname', 'users.email'); + }]); + + // If view is 'assigned', only show tasks assigned to the user + if ($request->query('view') !== 'all') { + $query->whereHas('users', function ($query) use ($userId) { + $query->where('users.id', $userId); + }); + } + + $tasks = $query->get(); + + $users = User::where('client_id', $clientId) + ->select('id', 'fname', 'mname', 'lname', 'email') + ->get(); + + return Inertia::render('member/member-task-list', [ + 'tasks' => $tasks, + 'users' => $users + ]); + } + + public function store(Request $request) + { + $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'required|string|max:255', + 'status' => 'required|in:pending,in_progress,completed', + 'priority' => 'required|in:low,medium,high', + 'due_date' => 'nullable|date', + 'users' => 'array', + 'users.*' => 'exists:users,id', + 'meeting_id' => 'nullable|exists:meetings,id' + ]); + + $task = Task::create([ + 'name' => $request->name, + 'description' => $request->description, + 'status' => $request->status, + 'priority' => $request->priority, + 'due_date' => $request->due_date, + 'client_id' => auth()->user()->client->id + ]); + + if ($request->has('users')) { + $task->users()->sync($request->users); + } + + if ($request->has('meeting_id')) { + $task->meetings()->sync([$request->meeting_id]); + } + + $timelineMessageService = new TimelineMessageService(); + $timelineMessageService->taskCreated(auth()->user(), $task->name, 'Task', $task->id); + + session()->flash('success', 'Task created successfully!'); + return back(); + } + + public function show(Task $task) + { + // Ensure the task belongs to the user's client and the user is assigned to it + if ($task->client_id !== auth()->user()->client->id || + !$task->users()->where('users.id', auth()->id())->exists()) { + abort(403, 'Unauthorized'); + } + + return Inertia::render('member/member-task-details', [ + 'task' => $task->load(['users' => function ($query) { + $query->where('users.client_id', auth()->user()->client->id) + ->select('users.id', 'users.fname', 'users.mname', 'users.lname', 'users.email'); + }]) + ]); + } + + public function update(Request $request, Task $task) + { + // Ensure the task belongs to the user's client and the user is assigned to it + if ($task->client_id !== auth()->user()->client->id || + !$task->users()->where('users.id', auth()->id())->exists()) { + abort(403, 'Unauthorized'); + } + + $validatedData = $request->validate([ + 'name' => 'sometimes|required|string|max:255', + 'description' => 'sometimes|required|string|max:255', + 'status' => 'sometimes|required|in:pending,in_progress,completed', + 'priority' => 'sometimes|required|in:low,medium,high', + 'due_date' => 'sometimes|nullable|date', + 'users' => 'sometimes|array', + 'users.*' => 'exists:users,id' + ]); + + $originalValues = $task->getOriginal(); + $task->update($validatedData); + + $timelineMessageService = new TimelineMessageService(); + $timelineMessageService->taskUpdated(auth()->user(), $task->name, 'Task', $task->id, $originalValues, $validatedData); + + session()->flash('success', 'Task updated successfully!'); + return back(); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Member/MemberDashboardController.php b/app/Http/Controllers/Member/MemberDashboardController.php new file mode 100644 index 0000000..efbb598 --- /dev/null +++ b/app/Http/Controllers/Member/MemberDashboardController.php @@ -0,0 +1,75 @@ +id(); + $clientId = auth()->user()->client->id; + + // Get tasks assigned to the user + $tasks = Task::where('client_id', $clientId) + ->whereHas('users', function ($query) use ($userId) { + $query->where('users.id', $userId); + }) + ->with(['users' => function ($query) use ($clientId) { + $query->where('users.client_id', $clientId) + ->select('users.id', 'users.fname', 'users.mname', 'users.lname', 'users.email'); + }]) + ->get(); + + // Get task completion statistics + $taskStats = [ + 'total' => $tasks->count(), + 'completed' => $tasks->where('status', 'completed')->count(), + 'in_progress' => $tasks->where('status', 'in_progress')->count(), + 'pending' => $tasks->where('status', 'pending')->count(), + ]; + + // Get upcoming tasks for this member + $upcomingTasks = $tasks->filter(function ($task) { + return $task->due_date && + Carbon::parse($task->due_date)->isBetween( + now(), + now()->addDays(7) + ); + })->values(); + + // Get meetings this member is invited to + $meetings = Meeting::where('client_id', $clientId) + ->whereHas('users', function ($query) use ($userId) { + $query->where('users.id', $userId); + }) + ->with(['users' => function ($query) use ($clientId) { + $query->where('users.client_id', $clientId) + ->select('users.id', 'users.fname', 'users.mname', 'users.lname', 'users.email'); + }]) + ->get(); + + // Get user's recent timeline activities + $recentActivities = Timeline::where('user_id', $userId) + ->with(['user:id,fname,mname,lname']) + ->latest() + ->take(10) + ->get(); + + return Inertia::render('member/member-dashboard', [ + 'taskStats' => $taskStats, + 'upcomingTasks' => $upcomingTasks, + 'meetings' => $meetings, + 'tasks' => $tasks, + 'recentActivities' => $recentActivities, + ]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Settings/ProfileController.php b/app/Http/Controllers/Settings/ProfileController.php index a6cb7e1..6fac33b 100644 --- a/app/Http/Controllers/Settings/ProfileController.php +++ b/app/Http/Controllers/Settings/ProfileController.php @@ -8,6 +8,7 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Inertia\Inertia; use Inertia\Response; @@ -29,13 +30,27 @@ public function edit(Request $request): Response */ public function update(ProfileUpdateRequest $request): RedirectResponse { - $request->user()->fill($request->validated()); + $validated = $request->validated(); + $user = $request->user(); + + // Update profile information + $user->fill([ + 'fname' => $validated['fname'], + 'mname' => $validated['mname'], + 'lname' => $validated['lname'], + 'email' => $validated['email'], + ]); + + if ($user->isDirty('email')) { + $user->email_verified_at = null; + } - if ($request->user()->isDirty('email')) { - $request->user()->email_verified_at = null; + // Update password if provided + if (isset($validated['password'])) { + $user->password = Hash::make($validated['password']); } - $request->user()->save(); + $user->save(); return to_route('profile.edit'); } diff --git a/app/Http/Middleware/CheckRole.php b/app/Http/Middleware/CheckRole.php index 1b29ed9..7aaeb9b 100644 --- a/app/Http/Middleware/CheckRole.php +++ b/app/Http/Middleware/CheckRole.php @@ -2,28 +2,30 @@ namespace App\Http\Middleware; +use App\Policies\RolePolicy; use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; class CheckRole { + protected $rolePolicy; + + public function __construct(RolePolicy $rolePolicy) + { + $this->rolePolicy = $rolePolicy; + } + /** * Handle an incoming request. * * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next */ - public function handle(Request $request, Closure $next, $role): Response + public function handle(Request $request, Closure $next, string $role): Response { $user = $request->user(); - // If not logged in or no role, deny - if (!$user || !$user->role) { - abort(403, 'Unauthorized'); - } - - // Check if user's role matches the required role - if ($user->role->name !== $role) { + if (!$user || !$this->rolePolicy->hasRole($user, $role)) { abort(403, 'Unauthorized'); } diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 3af9fd4..48a761e 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -44,7 +44,16 @@ public function share(Request $request): array 'name' => config('app.name'), 'quote' => ['message' => trim($message), 'author' => trim($author)], 'auth' => [ - 'user' => $request->user(), + 'user' => $request->user() ? [ + 'id' => $request->user()->id, + 'fname' => $request->user()->fname, + 'mname' => $request->user()->mname, + 'lname' => $request->user()->lname, + 'full_name' => $request->user()->full_name, + 'email' => $request->user()->email, + 'avatar' => $request->user()->avatar, + 'role' => $request->user()->role, + ] : null, ], 'ziggy' => fn (): array => [ ...(new Ziggy)->toArray(), diff --git a/app/Http/Requests/Settings/ProfileUpdateRequest.php b/app/Http/Requests/Settings/ProfileUpdateRequest.php index 64cf26b..2526b13 100644 --- a/app/Http/Requests/Settings/ProfileUpdateRequest.php +++ b/app/Http/Requests/Settings/ProfileUpdateRequest.php @@ -9,6 +9,11 @@ class ProfileUpdateRequest extends FormRequest { + public function authorize(): bool + { + return true; + } + /** * Get the validation rules that apply to the request. * @@ -17,8 +22,9 @@ class ProfileUpdateRequest extends FormRequest public function rules(): array { return [ - 'name' => ['required', 'string', 'max:255'], - + 'fname' => ['required', 'string', 'max:255'], + 'mname' => ['nullable', 'string', 'max:255'], + 'lname' => ['required', 'string', 'max:255'], 'email' => [ 'required', 'string', @@ -27,6 +33,9 @@ public function rules(): array 'max:255', Rule::unique(User::class)->ignore($this->user()->id), ], + 'current_password' => ['nullable', 'required_with:password', 'current_password'], + 'password' => ['nullable', 'string', 'min:8', 'confirmed'], + 'password_confirmation' => ['nullable', 'required_with:password', 'string'], ]; } } diff --git a/app/Listeners/UpdateTimelineTable.php b/app/Listeners/UpdateTimelineTable.php new file mode 100644 index 0000000..ad58a19 --- /dev/null +++ b/app/Listeners/UpdateTimelineTable.php @@ -0,0 +1,36 @@ + $event->user->id, + 'subject_type' => $event->subject_type, + 'subject_id' => $event->subject_id, + 'origin_client_id' => $event->origin_client_id, + 'message' => $event->message, + ]); + } +} \ No newline at end of file diff --git a/app/Models/Agenda.php b/app/Models/Agenda.php index b09609b..efd0a3e 100644 --- a/app/Models/Agenda.php +++ b/app/Models/Agenda.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\Relations\HasOne; + class Agenda extends Model { use SoftDeletes; diff --git a/app/Models/Client.php b/app/Models/Client.php index f57f856..b66e9fc 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -25,8 +25,8 @@ public function users(): HasMany return $this->hasMany(User::class); } - public function tasks(): BelongsToMany + public function tasks(): HasMany { - return $this->belongsToMany(Task::class); + return $this->hasMany(Task::class); } } diff --git a/app/Models/Meeting.php b/app/Models/Meeting.php index ef8d271..d92d1d3 100644 --- a/app/Models/Meeting.php +++ b/app/Models/Meeting.php @@ -32,11 +32,11 @@ public function client(): BelongsTo public function users(): BelongsToMany { - return $this->belongsToMany(User::class); + return $this->belongsToMany(User::class)->withTimestamps(); } public function tasks(): BelongsToMany { - return $this->belongsToMany(Task::class); + return $this->belongsToMany(Task::class)->withTimestamps(); } } diff --git a/app/Models/Role.php b/app/Models/Role.php index 20c2b46..2da315e 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -3,9 +3,12 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; class Role extends Model { + use SoftDeletes; + protected $fillable = ['name', 'description']; public function users() diff --git a/app/Models/Task.php b/app/Models/Task.php index 8f6819b..ed18f3f 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -3,6 +3,10 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\MorphMany; class Task extends Model { @@ -14,20 +18,26 @@ class Task extends Model 'status', 'priority', 'due_date', + 'client_id' ]; - public function users() + public function client(): BelongsTo { - return $this->belongsToMany(User::class); + return $this->belongsTo(Client::class); } - public function meetings() + public function users(): BelongsToMany { - return $this->belongsToMany(Meeting::class); + return $this->belongsToMany(User::class)->withTimestamps(); } - public function clients() + public function meetings(): BelongsToMany { - return $this->belongsToMany(Client::class); + return $this->belongsToMany(Meeting::class)->withTimestamps(); + } + + public function timelines(): MorphMany + { + return $this->morphMany(Timeline::class, 'subject'); } } diff --git a/app/Models/Timeline.php b/app/Models/Timeline.php new file mode 100644 index 0000000..f54db66 --- /dev/null +++ b/app/Models/Timeline.php @@ -0,0 +1,30 @@ +morphTo(); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function client(): BelongsTo + { + return $this->belongsTo(Client::class, 'origin_client_id'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 3727e95..b8cc5eb 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -8,11 +8,14 @@ use Illuminate\Notifications\Notifiable; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\SoftDeletes; class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable; + use HasFactory, Notifiable, SoftDeletes; + + protected $with = ['role']; /** * The attributes that are mass assignable. @@ -20,7 +23,9 @@ class User extends Authenticatable * @var list */ protected $fillable = [ - 'name', + 'fname', + 'mname', + 'lname', 'email', 'password', 'client_id', @@ -50,14 +55,22 @@ protected function casts(): array ]; } + /** + * Get the user's full name. + */ + public function getFullNameAttribute(): string + { + return trim(implode(' ', array_filter([$this->fname, $this->mname, $this->lname]))); + } + public function meetings(): BelongsToMany { - return $this->belongsToMany(Meeting::class); + return $this->belongsToMany(Meeting::class)->withTimestamps(); } public function tasks(): BelongsToMany { - return $this->belongsToMany(Task::class); + return $this->belongsToMany(Task::class)->withTimestamps(); } public function client(): BelongsTo @@ -69,4 +82,5 @@ public function role(): BelongsTo { return $this->belongsTo(Role::class); } + } diff --git a/app/Policies/RolePolicy.php b/app/Policies/RolePolicy.php new file mode 100644 index 0000000..c1b9f26 --- /dev/null +++ b/app/Policies/RolePolicy.php @@ -0,0 +1,35 @@ +role && $user->role->name === $role; + } + + /** + * Determine if the user is an admin. + */ + public function isAdmin(User $user): bool + { + return $this->hasRole($user, 'Client-Admin'); + } + + /** + * Determine if the user is a member. + */ + public function isMember(User $user): bool + { + return $this->hasRole($user, 'Member'); + } +} \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..eaf988a 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use Inertia\Inertia; class AppServiceProvider extends ServiceProvider { @@ -19,6 +20,12 @@ public function register(): void */ public function boot(): void { - // + Inertia::share([ + 'flash' => function () { + return [ + 'success' => session('success'), + ]; + }, + ]); } } diff --git a/app/Services/TimelineMessageService.php b/app/Services/TimelineMessageService.php new file mode 100644 index 0000000..788ce9c --- /dev/null +++ b/app/Services/TimelineMessageService.php @@ -0,0 +1,67 @@ +fname . " " . $user->lname; + Log::info('through service'); + event(new TimelineMessage($user, $message, $subjectType, $subjectId)); + } + + public function taskUpdated($user, string $message, string $subjectType, int $subjectId, $originalValues, $request): void + { + // Get the changed fields + $changedFields = []; + foreach ($request as $field => $value) { + if (isset($originalValues[$field]) && $originalValues[$field] !== $value) { + $changedFields[$field] = $value; + } + } + + $message = "Task updated by " . $user->fname . " " . $user->lname; + if (!empty($changedFields)) { + $fieldMessages = []; + foreach ($changedFields as $field => $value) { + switch ($field) { + case 'name': + $fieldMessages[] = "the name to `{$value}`"; + break; + case 'description': + $fieldMessages[] = "the description to `{$value}`"; + break; + case 'status': + if ($value == 'in_progress') {$value = 'in progress';} + $fieldMessages[] = "the status to `{$value}`"; + break; + case 'priority': + $fieldMessages[] = "the priority to `{$value}`"; + break; + case 'due_date': + $fieldMessages[] = "the due date to `{$value}`"; + break; + case 'users': + $fieldMessages[] = "the users assigned`"; + break; + + + } + } + + if (!empty($fieldMessages)) { + $lastField = array_pop($fieldMessages); + $message .= count($fieldMessages) > 0 + ? " by changing " . implode(', ', $fieldMessages) . " and " . $lastField + : " by changing " . $lastField; + } + } + + Log::info('through service'); + event(new TimelineMessage($user, $message, $subjectType, $subjectId)); + } +} \ No newline at end of file diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index e273a0c..3dad911 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -19,6 +19,7 @@ public function up(): void $table->string('password'); $table->boolean('password_set')->default(false); $table->rememberToken(); + $table->softDeletes(); $table->timestamps(); }); diff --git a/database/migrations/2025_04_29_063856_create_tasks_table.php b/database/migrations/2025_04_29_063856_create_tasks_table.php index 7eaf60b..42f88b8 100644 --- a/database/migrations/2025_04_29_063856_create_tasks_table.php +++ b/database/migrations/2025_04_29_063856_create_tasks_table.php @@ -13,6 +13,7 @@ public function up(): void { Schema::create('tasks', function (Blueprint $table) { $table->id(); + $table->foreignId('client_id')->constrained()->onDelete('cascade'); $table->string('name'); $table->string('description'); $table->enum('status', ['pending', 'in_progress', 'completed']); diff --git a/database/migrations/2025_04_29_064129_create_agendas_table.php b/database/migrations/2025_04_29_064129_create_agendas_table.php index aedeefc..1d40a60 100644 --- a/database/migrations/2025_04_29_064129_create_agendas_table.php +++ b/database/migrations/2025_04_29_064129_create_agendas_table.php @@ -14,7 +14,7 @@ public function up(): void { Schema::create('agendas', function (Blueprint $table) { $table->id(); - $table->string('text'); + $table->text('text'); $table->softDeletes(); $table->timestamps(); }); diff --git a/database/migrations/2025_04_30_181719_create_client_task_table.php b/database/migrations/2025_04_30_181719_create_client_task_table.php deleted file mode 100644 index dae705f..0000000 --- a/database/migrations/2025_04_30_181719_create_client_task_table.php +++ /dev/null @@ -1,31 +0,0 @@ -id(); - $table->foreignId('client_id')->constrained('clients'); - $table->foreignId('task_id')->constrained('tasks'); - $table->timestamps(); - $table->softDeletes(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('client_task'); - } -}; diff --git a/database/migrations/2025_05_30_163314_create_timelines_table.php b/database/migrations/2025_05_30_163314_create_timelines_table.php new file mode 100644 index 0000000..10a8cbb --- /dev/null +++ b/database/migrations/2025_05_30_163314_create_timelines_table.php @@ -0,0 +1,41 @@ +id(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + // Using short class name for polymorphic relationship (e.g., 'Task', 'Project', etc.) + $table->string('subject_type'); + $table->unsignedBigInteger('subject_id'); + $table->foreignId('origin_client_id')->constrained('clients')->onDelete('cascade'); + $table->text('message'); + $table->softDeletes(); + $table->timestamps(); + + // Add indexes for better performance + $table->index(['subject_type', 'subject_id']); + $table->index('origin_client_id'); + $table->index('created_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('timelines'); + } +}; diff --git a/database/migrations/2025_06_13_204535_split_user_name_into_parts.php b/database/migrations/2025_06_13_204535_split_user_name_into_parts.php new file mode 100644 index 0000000..0fddcd2 --- /dev/null +++ b/database/migrations/2025_06_13_204535_split_user_name_into_parts.php @@ -0,0 +1,74 @@ +string('fname')->after('id'); + $table->string('mname')->nullable()->after('fname'); + $table->string('lname')->after('mname'); + }); + + // Migrate existing data + DB::table('users')->orderBy('id')->chunk(100, function ($users) { + foreach ($users as $user) { + $nameParts = explode(' ', $user->name); + $fname = $nameParts[0] ?? ''; + $mname = $nameParts[1] ?? null; + $lname = $nameParts[2] ?? ''; + + // If there are more than 3 parts, add the rest to the last name + if (count($nameParts) > 3) { + $lname = implode(' ', array_slice($nameParts, 2)); + } + + DB::table('users') + ->where('id', $user->id) + ->update([ + 'fname' => $fname, + 'mname' => $mname, + 'lname' => $lname, + ]); + } + }); + + // Drop the old name column after data migration + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('name'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->string('name')->after('id'); + }); + + // Migrate data back + DB::table('users')->orderBy('id')->chunk(100, function ($users) { + foreach ($users as $user) { + $name = trim(implode(' ', array_filter([$user->fname, $user->mname, $user->lname]))); + + DB::table('users') + ->where('id', $user->id) + ->update(['name' => $name]); + } + }); + + Schema::table('users', function (Blueprint $table) { + $table->dropColumn(['fname', 'mname', 'lname']); + }); + } +}; diff --git a/database/seeders/AdditionalUsersSeeder.php b/database/seeders/AdditionalUsersSeeder.php new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/database/seeders/AdditionalUsersSeeder.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/database/seeders/AgendaSeeder.php b/database/seeders/AgendaSeeder.php new file mode 100644 index 0000000..5cdf1bd --- /dev/null +++ b/database/seeders/AgendaSeeder.php @@ -0,0 +1,26 @@ + 'Agenda 1', + ]); + + Agenda::create([ + 'text' => 'Agenda 2', + ]); + + Agenda::create([ + 'text' => 'Agenda 3', + ]); + } +} diff --git a/database/seeders/ClientSeeder.php b/database/seeders/ClientSeeder.php index 94c401e..0f90a4f 100644 --- a/database/seeders/ClientSeeder.php +++ b/database/seeders/ClientSeeder.php @@ -12,7 +12,7 @@ class ClientSeeder extends Seeder public function run() { Client::create([ - 'name' => 'Starplot', + 'name' => 'Equippers', ]); } } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 680f325..de6af81 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -16,6 +16,9 @@ public function run(): void ClientSeeder::class, SystemAdminSeeder::class, UserSeeder::class, + TaskSeeder::class, + AgendaSeeder::class, + MeetingSeeder::class, ]); } } diff --git a/database/seeders/MeetingSeeder.php b/database/seeders/MeetingSeeder.php new file mode 100644 index 0000000..1dcbeca --- /dev/null +++ b/database/seeders/MeetingSeeder.php @@ -0,0 +1,60 @@ +take(5)->get(); + + // Get some tasks to assign to meetings + $tasks = Task::where('client_id', 1)->take(5)->get(); + + $meetings = [ + [ + 'title' => 'Meeting 1', + 'date' => Carbon::now(), + 'type' => 'Online Meeting ', + 'agenda_id' => 1, + 'client_id' => 1, + ], + [ + 'title' => 'Meeting 2', + 'date' => Carbon::now(), + 'type' => 'In-Person Meeting', + 'agenda_id' => 2, + 'client_id' => 1, + ], + [ + 'title' => 'Meeting 3', + 'date' => Carbon::now(), + 'type' => 'In-Person Meeting', + 'agenda_id' => 3, + 'client_id' => 1, + ], + ]; + + foreach ($meetings as $meeting) { + $meeting = Meeting::create($meeting); + + $meeting->users()->attach( + $users->random(2)->pluck('id')->toArray(), + ['created_at' => now(), 'updated_at' => now()] + ); + + $meeting->tasks()->attach( + $tasks->random(2)->pluck('id')->toArray(), + ['created_at' => now(), 'updated_at' => now()] + ); + } + } +} diff --git a/database/seeders/SystemAdminSeeder.php b/database/seeders/SystemAdminSeeder.php index 9427307..ee60607 100644 --- a/database/seeders/SystemAdminSeeder.php +++ b/database/seeders/SystemAdminSeeder.php @@ -15,7 +15,8 @@ public function run() $adminUsers = [ [ - 'name' => 'System Admin', + 'fname' => 'System', + 'lname' => 'Admin', 'email' => 'systemadmin@lockedinc.com', 'role' => 'System-Admin', ], @@ -23,7 +24,8 @@ public function run() foreach ($adminUsers as $admin) { $userId = DB::table('users')->insertGetId([ - 'name' => $admin['name'], + 'fname' => $admin['fname'], + 'lname' => $admin['lname'], 'email' => $admin['email'], 'role_id' => 1, 'password' => Hash::make('password'), diff --git a/database/seeders/TaskSeeder.php b/database/seeders/TaskSeeder.php new file mode 100644 index 0000000..ab73c46 --- /dev/null +++ b/database/seeders/TaskSeeder.php @@ -0,0 +1,114 @@ +take(3)->get(); + + // Create 5 tasks for client 1 + $tasks = [ + [ + 'name' => 'Implement User Authentication', + 'description' => 'Set up secure user authentication system with JWT tokens', + 'status' => 'in_progress', + 'priority' => 'high', + 'due_date' => now()->addDays(7), + 'client_id' => 1, + ], + [ + 'name' => 'Design Database Schema', + 'description' => 'Create and optimize database schema for the application', + 'status' => 'completed', + 'priority' => 'high', + 'due_date' => now()->subDays(2), + 'client_id' => 1, + ], + [ + 'name' => 'Create API Documentation', + 'description' => 'Write comprehensive API documentation for all endpoints', + 'status' => 'pending', + 'priority' => 'medium', + 'due_date' => now()->addDays(14), + 'client_id' => 1, + ], + [ + 'name' => 'Implement Task Management', + 'description' => 'Build task management system with CRUD operations', + 'status' => 'in_progress', + 'priority' => 'high', + 'due_date' => now()->addDays(5), + 'client_id' => 1, + ], + [ + 'name' => 'Setup CI/CD Pipeline', + 'description' => 'Configure continuous integration and deployment pipeline', + 'status' => 'pending', + 'priority' => 'medium', + 'due_date' => now()->addDays(10), + 'client_id' => 1, + ], + [ + 'name' => 'Implement the new test', + 'description' => 'Build the new test', + 'status' => 'in_progress', + 'priority' => 'high', + 'due_date' => now()->addDays(5), + 'client_id' => 1, + ], + [ + 'name' => 'Implement New Object Detection Model', + 'description' => 'Build object detection model with YOLOv5', + 'status' => 'in_progress', + 'priority' => 'high', + 'due_date' => now()->addDays(5), + 'client_id' => 1, + ], + [ + 'name' => 'Start New Project', + 'description' => 'Start new project with Laravel and React', + 'status' => 'in_progress', + 'priority' => 'high', + 'due_date' => now()->addDays(5), + 'client_id' => 1, + ], + [ + 'name' => 'Diagnose and Fix Issue', + 'description' => 'Diagnose and fix issue with Laravel and React', + 'status' => 'in_progress', + 'priority' => 'high', + 'due_date' => now()->addDays(5), + 'client_id' => 1, + ], + + [ + 'name' => 'Transfer Data to New Database', + 'description' => 'Transfer data to new database', + 'status' => 'in_progress', + 'priority' => 'high', + 'due_date' => now()->addDays(5), + 'client_id' => 1, + ], + ]; + + foreach ($tasks as $taskData) { + $task = Task::create($taskData); + + // Assign 2 random users to each task + $task->users()->attach( + $users->random(2)->pluck('id')->toArray(), + ['created_at' => now(), 'updated_at' => now()] + ); + } + } +} \ No newline at end of file diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php index fef625f..626630e 100644 --- a/database/seeders/UserSeeder.php +++ b/database/seeders/UserSeeder.php @@ -12,28 +12,143 @@ class UserSeeder extends Seeder { public function run() { - - User::create([ - # client admin - 'name' => 'Ryan Mueller', - 'password' => Hash::make('password'), - 'email' => 'ryan@starplot.ai', - 'password_set' => true, - 'email_verified_at' => now(), - 'client_id' => 1, - 'role_id' => 2, + $users = [ + [ + 'fname' => 'Ryan', + 'mname' => 'Philip', + 'lname' => 'Mueller', + 'email' => 'ryan@equippers.com', + 'role_id' => 2, // Client-Admin + ], + [ + 'fname' => 'Andrew', + 'lname' => 'McDowell', + 'email' => 'andrew@equippers.com', + 'role_id' => 3, // Member + ], + [ + 'fname' => 'Allison', + 'mname' => 'Joyce', + 'lname' => 'Port', + 'email' => 'allison@equippers.com', + 'role_id' => 3, + ], + [ + 'fname' => 'George', + 'lname' => 'Kalafatis', + 'email' => 'george@equippers.com', + 'role_id' => 3, + ], + [ + 'fname' => 'John', + 'lname' => 'Doe', + 'email' => 'john@equippers.com', + 'role_id' => 3, + ], + [ + 'fname' => 'Sarah', + 'lname' => 'Johnson', + 'email' => 'sarah@equippers.com', + 'role_id' => 3, + ], + [ + 'fname' => 'Michael', + 'lname' => 'Chen', + 'email' => 'michael@equippers.com', + 'role_id' => 3, + ], + [ + 'fname' => 'Emma', + 'lname' => 'Wilson', + 'email' => 'emma@equippers.com', + 'role_id' => 3, + ], + [ + 'fname' => 'David', + 'lname' => 'Brown', + 'email' => 'david@equippers.com', + 'role_id' => 3, + ], + [ + 'fname' => 'Lisa', + 'lname' => 'Anderson', + 'email' => 'lisa@equippers.com', + 'role_id' => 3, + ], + [ + 'fname' => 'James', + 'lname' => 'Taylor', + 'email' => 'james@equippers.com', + 'role_id' => 3, + ], + [ + 'fname' => 'Rachel', + 'lname' => 'Martinez', + 'email' => 'rachel@equippers.com', + 'role_id' => 3, + ], + [ + 'fname' => 'Thomas', + 'lname' => 'White', + 'email' => 'thomas@equippers.com', + 'role_id' => 3, + ], + [ + 'fname' => 'Olivia', + 'lname' => 'Lee', + 'email' => 'olivia@equippers.com', + 'role_id' => 3, + ], + [ + 'fname' => 'Daniel', + 'lname' => 'Kim', + 'email' => 'daniel@equippers.com', + 'role_id' => 3, + ], + [ + 'fname' => 'Sophia', + 'lname' => 'Garcia', + 'email' => 'sophia@equippers.com', + 'role_id' => 3, + ], + [ + 'fname' => 'William', + 'lname' => 'Clark', + 'email' => 'william@equippers.com', + 'role_id' => 3, + ], + [ + 'fname' => 'Ava', + 'lname' => 'Rodriguez', + 'email' => 'ava@equippers.com', + 'role_id' => 3, + ], + [ + 'fname' => 'Ethan', + 'lname' => 'Moore', + 'email' => 'ethan@equippers.com', + 'role_id' => 3, + ], + [ + 'fname' => 'Isabella', + 'lname' => 'Thompson', + 'email' => 'isabella@equippers.com', + 'role_id' => 3, + ], + ]; - ]); - - User::create([ - # client member - 'name' => 'Andrew McDowell', - 'password' => Hash::make('password'), - 'email' => 'andrew@starplot.ai', - 'password_set' => true, - 'email_verified_at' => now(), - 'client_id' => 1, - 'role_id' => 3, - ]); + foreach ($users as $userData) { + User::create([ + 'fname' => $userData['fname'], + 'mname' => $userData['mname'] ?? null, + 'lname' => $userData['lname'], + 'email' => $userData['email'], + 'password' => Hash::make('password'), + 'password_set' => true, + 'email_verified_at' => now(), + 'client_id' => 1, + 'role_id' => $userData['role_id'], + ]); + } } } diff --git a/package-lock.json b/package-lock.json index ddc58af..08d7203 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,13 +7,16 @@ "dependencies": { "@headlessui/react": "^2.2.0", "@inertiajs/react": "^2.0.0", + "@nivo/pie": "^0.99.0", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3", - "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-navigation-menu": "^1.2.5", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", @@ -27,12 +30,15 @@ "@vitejs/plugin-react": "^4.3.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "concurrently": "^9.0.1", + "date-fns": "^4.1.0", "globals": "^15.14.0", "laravel-vite-plugin": "^1.0", "lucide-react": "^0.475.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "recharts": "^2.15.3", "sonner": "^2.0.3", "tailwind-merge": "^3.0.1", "tailwindcss": "^4.0.0", @@ -281,6 +287,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", @@ -1086,6 +1101,150 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nivo/arcs": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/arcs/-/arcs-0.99.0.tgz", + "integrity": "sha512-UcvWLQPl+A3APk2Gm74N5xDfT+ATnVs2XkP73WxhYPWJk+dBzF00cndA5g/dptOwdFBvvo62VgcCsNiwUsjKTw==", + "license": "MIT", + "dependencies": { + "@nivo/colors": "0.99.0", + "@nivo/core": "0.99.0", + "@nivo/text": "0.99.0", + "@nivo/theming": "0.99.0", + "@react-spring/core": "9.4.5 || ^9.7.2 || ^10.0", + "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", + "@types/d3-shape": "^3.1.6", + "d3-shape": "^3.2.0" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/@nivo/colors": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/colors/-/colors-0.99.0.tgz", + "integrity": "sha512-hyYt4lEFIfXOUmQ6k3HXm3KwhcgoJpocmoGzLUqzk7DzuhQYJo+4d5jIGGU0N/a70+9XbHIdpKNSblHAIASD3w==", + "license": "MIT", + "dependencies": { + "@nivo/core": "0.99.0", + "@nivo/theming": "0.99.0", + "@types/d3-color": "^3.0.0", + "@types/d3-scale": "^4.0.8", + "@types/d3-scale-chromatic": "^3.0.0", + "d3-color": "^3.1.0", + "d3-scale": "^4.0.2", + "d3-scale-chromatic": "^3.0.0", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/@nivo/core": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/core/-/core-0.99.0.tgz", + "integrity": "sha512-olCItqhPG3xHL5ei+vg52aB6o+6S+xR2idpkd9RormTTUniZb8U2rOdcQojOojPY5i9kVeQyLFBpV4YfM7OZ9g==", + "license": "MIT", + "dependencies": { + "@nivo/theming": "0.99.0", + "@nivo/tooltip": "0.99.0", + "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", + "@types/d3-shape": "^3.1.6", + "d3-color": "^3.1.0", + "d3-format": "^1.4.4", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-scale-chromatic": "^3.0.0", + "d3-shape": "^3.2.0", + "d3-time-format": "^3.0.0", + "lodash": "^4.17.21", + "react-virtualized-auto-sizer": "^1.0.26", + "use-debounce": "^10.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nivo/donate" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/@nivo/legends": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/legends/-/legends-0.99.0.tgz", + "integrity": "sha512-P16FjFqNceuTTZphINAh5p0RF0opu3cCKoWppe2aRD9IuVkvRm/wS5K1YwMCxDzKyKh5v0AuTlu9K6o3/hk8hA==", + "license": "MIT", + "dependencies": { + "@nivo/colors": "0.99.0", + "@nivo/core": "0.99.0", + "@nivo/text": "0.99.0", + "@nivo/theming": "0.99.0", + "@types/d3-scale": "^4.0.8", + "d3-scale": "^4.0.2" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/@nivo/pie": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/pie/-/pie-0.99.0.tgz", + "integrity": "sha512-zUbo8UdLndp2RMljrOqitAKKEnl7YypkJrOzjKLk8jQGU7qqUKtgFoJIPhiBsvNPs3xtX2KwgtS1+JKNTNns7A==", + "license": "MIT", + "dependencies": { + "@nivo/arcs": "0.99.0", + "@nivo/colors": "0.99.0", + "@nivo/core": "0.99.0", + "@nivo/legends": "0.99.0", + "@nivo/theming": "0.99.0", + "@nivo/tooltip": "0.99.0", + "@types/d3-shape": "^3.1.6", + "d3-shape": "^3.2.0" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/@nivo/text": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/text/-/text-0.99.0.tgz", + "integrity": "sha512-ho3oZpAZApsJNjsIL5WJSAdg/wjzTBcwo1KiHBlRGUmD+yUWO8qp7V+mnYRhJchwygtRVALlPgZ/rlcW2Xr/MQ==", + "license": "MIT", + "dependencies": { + "@nivo/core": "0.99.0", + "@nivo/theming": "0.99.0", + "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/@nivo/theming": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/theming/-/theming-0.99.0.tgz", + "integrity": "sha512-KvXlf0nqBzh/g2hAIV9bzscYvpq1uuO3TnFN3RDXGI72CrbbZFTGzprPju3sy/myVsauv+Bb+V4f5TZ0jkYKRg==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/@nivo/tooltip": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/tooltip/-/tooltip-0.99.0.tgz", + "integrity": "sha512-weoEGR3xAetV4k2P6k96cdamGzKQ5F2Pq+uyDaHr1P3HYArM879Pl+x+TkU0aWjP6wgUZPx/GOBiV1Hb1JxIqg==", + "license": "MIT", + "dependencies": { + "@nivo/core": "0.99.0", + "@nivo/theming": "0.99.0", + "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1302,23 +1461,23 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", - "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", + "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, @@ -1337,10 +1496,16 @@ } } }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", - "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1352,46 +1517,32 @@ } } }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", - "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-escape-keydown": "1.1.0" - }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz", - "integrity": "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-menu": "2.1.6", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-controllable-state": "1.1.0" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1408,10 +1559,10 @@ } } }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", - "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1423,15 +1574,15 @@ } } }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", - "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1448,13 +1599,13 @@ } } }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1466,13 +1617,14 @@ } } }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.2.tgz", - "integrity": "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.2" + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1489,30 +1641,14 @@ } } }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.6.tgz", - "integrity": "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.2", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-roving-focus": "1.1.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1529,26 +1665,13 @@ } } }, - "node_modules/@radix-ui/react-navigation-menu": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.5.tgz", - "integrity": "sha512-myMHHQUZ3ZLTi8W381/Vu43Ia0NqakkQZ2vzynMmTUtQQ9kNkjzhOwkZC9TAM5R07OZUVIQyHC06f/9JZJpvvA==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.2" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -1565,130 +1688,949 @@ } } }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", - "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-rect": "1.1.0", - "@radix-ui/react-use-size": "1.1.0", - "@radix-ui/rect": "1.1.0" + "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", - "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", - "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.0" + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", - "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.1.2" + "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", - "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz", + "integrity": "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", + "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.2.tgz", + "integrity": "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.6.tgz", + "integrity": "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.5.tgz", + "integrity": "sha512-myMHHQUZ3ZLTi8W381/Vu43Ia0NqakkQZ2vzynMmTUtQQ9kNkjzhOwkZC9TAM5R07OZUVIQyHC06f/9JZJpvvA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", + "integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", + "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", + "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", + "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1886,14 +2828,47 @@ } } }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" - }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -2082,6 +3057,78 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, + "node_modules/@react-spring/animated": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-10.0.1.tgz", + "integrity": "sha512-BGL3hA66Y8Qm3KmRZUlfG/mFbDPYajgil2/jOP0VXf2+o2WPVmcDps/eEgdDqgf5Pv9eBbyj7LschLMuSjlW3Q==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-10.0.1.tgz", + "integrity": "sha512-KaMMsN1qHuVTsFpg/5ajAVye7OEqhYbCq0g4aKM9bnSZlDBBYpO7Uf+9eixyXN8YEbF+YXaYj9eoWDs+npZ+sA==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~10.0.1", + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-10.0.1.tgz", + "integrity": "sha512-UrzG/d6Is+9i0aCAjsjWRqIlFFiC4lFqFHrH63zK935z2YDU95TOFio4VKGISJ5SG0xq4ULy7c1V3KU+XvL+Yg==", + "license": "MIT" + }, + "node_modules/@react-spring/shared": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-10.0.1.tgz", + "integrity": "sha512-KR2tmjDShPruI/GGPfAZOOLvDgkhFseabjvxzZFFggJMPkyICLjO0J6mCIoGtdJSuHywZyc4Mmlgi+C88lS00g==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-10.0.1.tgz", + "integrity": "sha512-Fk1wYVAKL+ZTYK+4YFDpHf3Slsy59pfFFvnnTfRjQQFGlyIo4VejPtDs3CbDiuBjM135YztRyZjIH2VbycB+ZQ==", + "license": "MIT" + }, + "node_modules/@react-spring/web": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-10.0.1.tgz", + "integrity": "sha512-FgQk02OqFrYyJBTTnBTWAU0WPzkHkKXauc6aeexcvATvLapUxwnfGuLlsLYF8BYjEVfkivPT04ziAue6zyRBtQ==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~10.0.1", + "@react-spring/core": "~10.0.1", + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@react-stately/utils": { "version": "3.10.5", "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz", @@ -2684,6 +3731,75 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -3429,6 +4545,22 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3518,6 +4650,158 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", + "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-time": "1 - 2" + } + }, + "node_modules/d3-time-format/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-time-format/node_modules/d3-time": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz", + "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "2" + } + }, + "node_modules/d3-time-format/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -3572,6 +4856,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -3589,6 +4883,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3681,6 +4981,16 @@ "node": ">=0.10.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4165,6 +5475,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4172,6 +5488,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -4686,6 +6011,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -5505,7 +6839,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -5640,7 +6973,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6032,7 +7364,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -6117,7 +7448,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/react-refresh": { @@ -6176,6 +7506,21 @@ } } }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -6198,6 +7543,70 @@ } } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/react-virtualized-auto-sizer": { + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz", + "integrity": "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A==", + "license": "MIT", + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz", + "integrity": "sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6824,6 +8233,12 @@ "node": ">=6" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7079,6 +8494,18 @@ } } }, + "node_modules/use-debounce": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.4.tgz", + "integrity": "sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==", + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/use-sidecar": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", @@ -7101,6 +8528,28 @@ } } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", diff --git a/package.json b/package.json index 693fd0e..8f0fb8f 100644 --- a/package.json +++ b/package.json @@ -25,13 +25,16 @@ "dependencies": { "@headlessui/react": "^2.2.0", "@inertiajs/react": "^2.0.0", + "@nivo/pie": "^0.99.0", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3", - "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-navigation-menu": "^1.2.5", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", @@ -45,12 +48,15 @@ "@vitejs/plugin-react": "^4.3.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "concurrently": "^9.0.1", + "date-fns": "^4.1.0", "globals": "^15.14.0", "laravel-vite-plugin": "^1.0", "lucide-react": "^0.475.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "recharts": "^2.15.3", "sonner": "^2.0.3", "tailwind-merge": "^3.0.1", "tailwindcss": "^4.0.0", diff --git a/resources/js/components/app-header.tsx b/resources/js/components/app-header.tsx index 8edadcf..cd9a904 100644 --- a/resources/js/components/app-header.tsx +++ b/resources/js/components/app-header.tsx @@ -86,8 +86,8 @@ export function AppHeader({ breadcrumbs = [] }: AppHeaderProps) { diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index 05ba0e8..380a33a 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -1,37 +1,24 @@ import { NavMain } from '@/components/nav-main'; import { NavUser } from '@/components/nav-user'; import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'; -import { type NavItem } from '@/types'; -import { Link } from '@inertiajs/react'; -import { LayoutGrid, SquareCheckBig, Users } from 'lucide-react'; +import { type PageProps, type UserRole } from '@/types'; +import { Link, usePage } from '@inertiajs/react'; import AppLogo from './app-logo'; - -const mainNavItems: NavItem[] = [ - { - title: 'Dashboard', - href: '/dashboard', - icon: LayoutGrid, - }, - { - title: 'Tasks', - href: '/tasks', - icon: SquareCheckBig, - }, - { - title: 'Users', - href: '/users', - icon: Users, - }, -]; +import { getNavigationItems } from '@/config/navigation'; export function AppSidebar() { + const props = usePage().props as unknown as PageProps; + const userRole = props.auth.user.role?.name as UserRole | undefined; + const navItems = getNavigationItems(userRole); + const dashboardPath = userRole === 'Client-Admin' ? '/admin/dashboard' : '/member/dashboard'; + return ( - + @@ -40,7 +27,7 @@ export function AppSidebar() { - + diff --git a/resources/js/components/dashboard/admin-recent-activity.tsx b/resources/js/components/dashboard/admin-recent-activity.tsx new file mode 100644 index 0000000..8e640c2 --- /dev/null +++ b/resources/js/components/dashboard/admin-recent-activity.tsx @@ -0,0 +1,66 @@ +import { Timeline } from '@/types/timeline'; +import { format } from 'date-fns'; +import { Clock } from 'lucide-react'; +import { router } from '@inertiajs/react'; +import { useRolePrefix } from '@/hooks/use-role-prefix'; + +interface RecentActivityProps { + activities: Timeline[]; +} + +export function RecentActivity({ activities }: RecentActivityProps) { + const { getRoute } = useRolePrefix(); + + const handleActivityClick = (activity: Timeline) => { + switch (activity.subject_type) { + case 'Task': + router.visit(getRoute(`/tasks/${activity.subject_id}`)); + break; + case 'Meeting': + router.visit(getRoute(`/meetings/${activity.subject_id}`)); + break; + // Add more cases for other subject types as needed + default: + console.log('Unknown subject type:', activity.subject_type); + } + }; + + if (activities.length === 0) { + return ( +
+ No recent activity +
+ ); + } + + return ( +
+ {activities.map((activity) => ( +
handleActivityClick(activity)} + className="group relative flex items-start gap-3 p-3 rounded-lg transition-all duration-200 hover:bg-muted/50 cursor-pointer" + > +
+ +
+
+

+ {activity.message} +

+
+

+ {format(new Date(activity.created_at), 'MMM d, yyyy • h:mm a')} +

+ {activity.user && ( + + • by {activity.user.fname} {activity.user.lname} + + )} +
+
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/resources/js/components/dashboard/member-performance-metrics.tsx b/resources/js/components/dashboard/member-performance-metrics.tsx new file mode 100644 index 0000000..02b2af8 --- /dev/null +++ b/resources/js/components/dashboard/member-performance-metrics.tsx @@ -0,0 +1,48 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { CheckCircle2, Clock, AlertCircle, BarChart3 } from 'lucide-react'; + +interface TaskStats { + total: number; + completed: number; + in_progress: number; + pending: number; +} + +interface PerformanceMetricsProps { + taskStats: TaskStats; +} + +export function PerformanceMetrics({ taskStats }: PerformanceMetricsProps) { + return ( +
+
+ +
+
Completed
+
{taskStats.completed}
+
+
+
+ +
+
In Progress
+
{taskStats.in_progress}
+
+
+
+ +
+
Pending
+
{taskStats.pending}
+
+
+
+ +
+
Total
+
{taskStats.total}
+
+
+
+ ); +} \ No newline at end of file diff --git a/resources/js/components/dashboard/member-recent-activity.tsx b/resources/js/components/dashboard/member-recent-activity.tsx new file mode 100644 index 0000000..728bdeb --- /dev/null +++ b/resources/js/components/dashboard/member-recent-activity.tsx @@ -0,0 +1,64 @@ +import { Timeline } from '@/types/timeline'; +import { format } from 'date-fns'; +import { Clock, CheckCircle2, AlertCircle, Calendar, User } from 'lucide-react'; +import { router } from '@inertiajs/react'; +import { useRolePrefix } from '@/hooks/use-role-prefix'; + +interface RecentActivityProps { + activities: Timeline[]; +} + +export function MemberRecentActivity({ activities }: RecentActivityProps) { + const { getRoute } = useRolePrefix(); + + const getActivityIcon = (subjectType: string) => { + switch (subjectType.toLowerCase()) { + case 'task': + return ; + case 'meeting': + return ; + default: + return ; + } + }; + + const handleActivityClick = (activity: Timeline) => { + const route = getRoute(`/${activity.subject_type.toLowerCase()}s/${activity.subject_id}`); + router.visit(route); + }; + + if (activities.length === 0) { + return ( +
No recent activity
+ ); + } + + return ( +
    + {activities.map((activity) => ( +
  • handleActivityClick(activity)} + > +
    + {getActivityIcon(activity.subject_type)} +
    +
    +

    {activity.message}

    +
    + + + {activity.user?.fname} {activity.user?.lname} + + + + {format(new Date(activity.created_at), 'MMM d, h:mm a')} + +
    +
    +
  • + ))} +
+ ); +} \ No newline at end of file diff --git a/resources/js/components/dashboard/member-watch-list.tsx b/resources/js/components/dashboard/member-watch-list.tsx new file mode 100644 index 0000000..5875d8b --- /dev/null +++ b/resources/js/components/dashboard/member-watch-list.tsx @@ -0,0 +1,89 @@ +import { Eye, Calendar, Clock, AlertCircle } from 'lucide-react'; +import { format, isPast, addHours, isBefore } from 'date-fns'; + +interface Task { + id: number; + name: string; + status: 'pending' | 'in_progress' | 'completed'; + due_date: string; + priority: 'low' | 'medium' | 'high'; +} + +interface MemberWatchListProps { + tasks: Task[]; +} + +function getStatusBadge(status: string) { + const map: Record = { + completed: 'bg-green-100 text-green-800', + in_progress: 'bg-blue-100 text-blue-800', + pending: 'bg-yellow-100 text-yellow-800', + }; + return map[status] || 'bg-gray-100 text-gray-800'; +} + +function getPriorityBadge(priority: string) { + const map: Record = { + high: 'bg-red-100 text-red-800', + medium: 'bg-orange-100 text-orange-800', + low: 'bg-green-100 text-green-800', + }; + return map[priority] || 'bg-gray-100 text-gray-800'; +} + +function isDueSoon(dueDate: string): boolean { + const date = new Date(dueDate); + const now = new Date(); + const twentyFourHoursFromNow = addHours(now, 24); + return !isPast(date) && isBefore(date, twentyFourHoursFromNow); +} + +export function MemberWatchList({ tasks }: MemberWatchListProps) { + // Sort tasks by due date and filter out completed tasks + const sortedTasks = [...tasks] + .filter(task => task.status !== 'completed') + .sort((a, b) => new Date(a.due_date).getTime() - new Date(b.due_date).getTime()) + .slice(0, 3); // Only show the first 3 tasks + + if (sortedTasks.length === 0) { + return ( +
No upcoming tasks
+ ); + } + + return ( +
    + {sortedTasks.map(task => { + const isAlmostDue = isDueSoon(task.due_date); + return ( +
  • +
    + + {task.name} + {isAlmostDue && ( + + )} +
    + +
    + + {task.status.replace('_', ' ')} + + + {task.priority} + +
    + + {format(new Date(task.due_date), 'MMM d, yyyy')} +
    +
    + + {format(new Date(task.due_date), 'h:mm a')} +
    +
    +
  • + ); + })} +
+ ); +} \ No newline at end of file diff --git a/resources/js/components/dashboard/performance-metrics.tsx b/resources/js/components/dashboard/performance-metrics.tsx new file mode 100644 index 0000000..5f63e83 --- /dev/null +++ b/resources/js/components/dashboard/performance-metrics.tsx @@ -0,0 +1,50 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Progress } from '@/components/ui/progress'; + +interface TaskStats { + total: number; + completed: number; + in_progress: number; + pending: number; +} + +interface PerformanceMetricsProps { + taskStats: TaskStats; +} + +export function PerformanceMetrics({ taskStats }: PerformanceMetricsProps) { + const completion = taskStats.total > 0 ? Math.round((taskStats.completed / taskStats.total) * 100) : 0; + + return ( + + + Performance Metric + + +
+
+
{taskStats.completed}
+
Completed
+
+
+
{taskStats.in_progress}
+
In Progress
+
+
+
{taskStats.pending}
+
Pending
+
+
+
{taskStats.total}
+
Total
+
+
+
+ + {completion}% +
+
Task completion rate
+
+
+ ); +} \ No newline at end of file diff --git a/resources/js/components/dashboard/recent-activity.tsx b/resources/js/components/dashboard/recent-activity.tsx new file mode 100644 index 0000000..e43bfc1 --- /dev/null +++ b/resources/js/components/dashboard/recent-activity.tsx @@ -0,0 +1,48 @@ +import { Timeline } from '@/types/timeline'; +import { format } from 'date-fns'; +import { Clock } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface RecentActivityProps { + activities: Timeline[]; +} + +export function RecentActivity({ activities }: RecentActivityProps) { + if (activities.length === 0) { + return ( +
+ No recent activity +
+ ); + } + + return ( +
+ {activities.map((activity) => ( +
+
+ +
+
+

+ {activity.message} +

+
+

+ {format(new Date(activity.created_at), 'MMM d, yyyy • h:mm a')} +

+ {activity.user && ( + + • by {activity.user.fname} {activity.user.lname} + + )} +
+
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/resources/js/components/dashboard/task-stats-pie.tsx b/resources/js/components/dashboard/task-stats-pie.tsx new file mode 100644 index 0000000..3d77f86 --- /dev/null +++ b/resources/js/components/dashboard/task-stats-pie.tsx @@ -0,0 +1,85 @@ +import { ResponsivePie } from '@nivo/pie'; +import { router } from '@inertiajs/react'; +import { useRolePrefix } from '@/hooks/use-role-prefix'; + +interface TaskStats { + completed: number; + in_progress: number; + pending: number; +} + +export function TaskStatusPieChart({ taskStats }: { taskStats: TaskStats }) { + const { getRoute } = useRolePrefix(); + const pieData = [ + { + id: 'Completed', + label: 'Completed', + value: taskStats.completed, + color: '#22c55e', // green + }, + { + id: 'In Progress', + label: 'In Progress', + value: taskStats.in_progress, + color: '#3b82f6', // blue + }, + { + id: 'Pending', + label: 'Pending', + value: taskStats.pending, + color: '#facc15', // yellow + }, + ]; + + const handlePieClick = (data: any) => { + if (data && data.id) { + let status = ''; + if (data.id === 'Completed') status = 'completed'; + else if (data.id === 'In Progress') status = 'in_progress'; + else if (data.id === 'Pending') status = 'pending'; + if (status) { + router.visit(getRoute(`/tasks?status=${status}`)); + } + } + }; + + return ( +
+ + {/* Custom Legend */} +
+ + + +
+
+ ); +} + +function LegendItem({ color, label }: { color: string; label: string }) { + return ( +
+ + {label} +
+ ); +} diff --git a/resources/js/components/dashboard/user-workload-chart.tsx b/resources/js/components/dashboard/user-workload-chart.tsx new file mode 100644 index 0000000..a792474 --- /dev/null +++ b/resources/js/components/dashboard/user-workload-chart.tsx @@ -0,0 +1,80 @@ +import { Task, User } from '@/types/task'; +import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis, Tooltip } from 'recharts'; +import { useState } from 'react'; + +interface UserWorkloadChartProps { + tasks: Task[]; + users: User[]; +} + +export function UserWorkloadChart({ tasks, users }: UserWorkloadChartProps) { + // Calculate workload for each user + const workloadData = users.map(user => { + const userTasks = tasks.filter(task => + task.users?.some(u => u.id === user.id) + ); + + return { + name: user.fname + ' ' + user.lname, + total: userTasks.length, + }; + }); + + // Sort by total workload descending + const sortedData = [...workloadData].sort((a, b) => b.total - a.total); + const [startIndex, setStartIndex] = useState(0); + const usersPerPage = 6; + const maxIndex = Math.max(0, sortedData.length - usersPerPage); + const visibleData = sortedData.slice(startIndex, startIndex + usersPerPage); + + return ( +
+ + + + + + + + + {sortedData.length > usersPerPage && ( +
+ setStartIndex(Number(e.target.value))} + step={1} + style={{ width: '60%' }} + /> + + Showing {startIndex + 1}-{Math.min(startIndex + usersPerPage, sortedData.length)} of {sortedData.length} + +
+ )} +
+ ); +} \ No newline at end of file diff --git a/resources/js/components/data-table.tsx b/resources/js/components/data-table.tsx index d704a70..d8d19a7 100644 --- a/resources/js/components/data-table.tsx +++ b/resources/js/components/data-table.tsx @@ -46,6 +46,7 @@ interface DataTableProps { searchPlaceholder?: string searchColumn?: string onRowClick?: (row: TData) => void; // allow for row click to lead to action + addButton?: React.ReactNode // optional add button } export function DataTable({ @@ -54,6 +55,7 @@ export function DataTable({ searchPlaceholder = "Filter...", searchColumn = "email", onRowClick, + addButton, }: DataTableProps) { const [sorting, setSorting] = React.useState([]) const [columnFilters, setColumnFilters] = React.useState([]) @@ -81,7 +83,7 @@ export function DataTable({ return (
-
+
({ } className="max-w-sm" /> - - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ) - })} - - + {addButton &&
{addButton}
}
@@ -180,6 +157,7 @@ export function DataTable({ size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()} + className="cursor-pointer" > Previous @@ -188,6 +166,7 @@ export function DataTable({ size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()} + className="cursor-pointer" > Next diff --git a/resources/js/components/meetings/create-meeting-dialog.tsx b/resources/js/components/meetings/create-meeting-dialog.tsx new file mode 100644 index 0000000..cd30996 --- /dev/null +++ b/resources/js/components/meetings/create-meeting-dialog.tsx @@ -0,0 +1,121 @@ +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { Input } from "@/components/ui/input" +import { useForm } from "@inertiajs/react" +import { User, Task } from "@/types/task" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { MultiSelect } from "@/components/ui/multi-select" +import { useRolePrefix } from "@/hooks/use-role-prefix" +interface CreateMeetingDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + users: User[] + tasks: Task[] +} + +export function CreateMeetingDialog({ open, onOpenChange, users, tasks }: CreateMeetingDialogProps) { + const { getRoute } = useRolePrefix(); + const { data, setData, post, processing, errors, reset } = useForm({ + title: '', + date: '', + type: '', + agenda_text: '', + users: [] as number[], + tasks: [] as number[], + }) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + post(getRoute('/meetings'), { + onSuccess: () => { + reset() + onOpenChange(false) + } + }) + } + + return ( + + +
+ + Create New Meeting + + Add a new meeting and assign attendees and tasks. + + +
+
+
+ + setData('title', e.target.value)} + error={errors.title} + /> +
+
+ + setData('date', e.target.value)} + error={errors.date} + /> +
+
+ + setData('type', e.target.value)} + error={errors.type} + placeholder="Type (e.g. Internal, Client, etc.)" + /> +
+
+ + setData('agenda_text', e.target.value)} + error={errors.agenda_text} + placeholder="Meeting agenda or description" + /> +
+
+
+ setData('users', ids)} + placeholder="Select users..." + label="Attendees" + error={errors.users} + /> + setData('tasks', ids)} + placeholder="Select tasks..." + label="Related Tasks" + error={errors.tasks} + /> +
+
+ + + + + +
+
+ ) +} \ No newline at end of file diff --git a/resources/js/components/meetings/meeting-summary.tsx b/resources/js/components/meetings/meeting-summary.tsx new file mode 100644 index 0000000..1ba02b5 --- /dev/null +++ b/resources/js/components/meetings/meeting-summary.tsx @@ -0,0 +1,66 @@ +import { Meeting } from "@/types/meeting" +import { Card } from "@/components/ui/card" +import { Users, Calendar, Clock } from "lucide-react" + +interface MeetingSummaryProps { + meetings: Meeting[] +} + +export function MeetingSummary({ meetings }: MeetingSummaryProps) { + const now = new Date() + const upcomingCount = meetings.filter(m => new Date(m.date) > now).length + const pastCount = meetings.filter(m => new Date(m.date) <= now).length + const totalCount = meetings.length + + const totalAttendees = meetings.reduce((acc, meeting) => acc + meeting.users.length, 0) + const averageAttendees = totalCount > 0 ? Math.round(totalAttendees / totalCount) : 0 + + return ( + +
+
+
+
+ + + {upcomingCount} + +
+ Upcoming +
+ +
+
+ + + {pastCount} + +
+ Past +
+ +
+
+ + + {averageAttendees} + +
+ Avg. Attendees +
+ +
+ +
+
+ + {totalCount} + +
+ Total Meetings +
+
+
+ + ) +} \ No newline at end of file diff --git a/resources/js/components/tasks/admin-task-stats.tsx b/resources/js/components/tasks/admin-task-stats.tsx new file mode 100644 index 0000000..145985a --- /dev/null +++ b/resources/js/components/tasks/admin-task-stats.tsx @@ -0,0 +1,12 @@ +import { Task } from "@/types/task" +import { TaskSummary } from "@/components/tasks/task-summary" + +interface AdminTaskStatsProps { + tasks: Task[] +} + +export function AdminTaskStats({ tasks }: AdminTaskStatsProps) { + return ( + + ) +} \ No newline at end of file diff --git a/resources/js/components/tasks/create-task-dialog.tsx b/resources/js/components/tasks/create-task-dialog.tsx new file mode 100644 index 0000000..083a255 --- /dev/null +++ b/resources/js/components/tasks/create-task-dialog.tsx @@ -0,0 +1,155 @@ +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { useForm, usePage } from "@inertiajs/react" +import { TaskStatus, TaskPriority, User } from "@/types/task" +import { MultiSelect } from "@/components/ui/multi-select" +import { type PageProps } from '@/types' + +interface CreateTaskDialogProps { + users: User[]; + open: boolean; + onOpenChange: (open: boolean) => void; + meetingId?: number; +} + +export function CreateTaskDialog({ users, open, onOpenChange, meetingId }: CreateTaskDialogProps) { + const { data, setData, post, processing, errors, reset } = useForm({ + name: '', + description: '', + status: 'pending' as TaskStatus, + priority: 'medium' as TaskPriority, + due_date: '', + users: [] as number[], + meeting_id: meetingId + }); + + const { auth } = usePage().props as unknown as PageProps; + const isAdmin = auth.user.role?.name === 'Client-Admin'; + const prefix = isAdmin ? '/admin' : '/member'; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + post(`${prefix}/tasks`, { + onSuccess: () => { + reset(); + onOpenChange(false); + window.location.reload(); + } + }); + }; + + return ( + + + + Create New Task + + Add a new task to your project. Fill in the details below. + + +
+
+
+
+ + setData('name', e.target.value)} + error={errors.name} + /> +
+ +
+ +