diff --git a/app/Http/Controllers/UsersController.php b/app/Http/Controllers/UsersController.php index 28d87d91..948e7800 100644 --- a/app/Http/Controllers/UsersController.php +++ b/app/Http/Controllers/UsersController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Http\Controllers\Controller; +use App\Http\Requests\StoreUserRequest; use App\Http\Requests\UpdateUserRequest; use App\Models\User; use App\Services\UserService; @@ -28,6 +29,16 @@ class UsersController extends Controller return Inertia::render('dashboard/users/index', compact('users')); } + /** + * Store a newly created user and send an invite. + */ + public function store(StoreUserRequest $request): RedirectResponse + { + $this->userService->inviteUser($request->validated()); + + return redirect()->back()->with('success', __('dashboard.user_invited')); + } + /** * Update the user's account. */ diff --git a/app/Http/Requests/StoreUserRequest.php b/app/Http/Requests/StoreUserRequest.php new file mode 100644 index 00000000..c9c08299 --- /dev/null +++ b/app/Http/Requests/StoreUserRequest.php @@ -0,0 +1,30 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:users,email'], + 'status' => ['required', 'integer', 'in:0,1'], + ]; + } +} diff --git a/app/Services/UserService.php b/app/Services/UserService.php index e2e19a01..99594abc 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -2,10 +2,15 @@ namespace App\Services; +use App\Enums\UserType; use App\Models\User; +use App\Notifications\ResetPasswordNotification; use Illuminate\Support\Facades\DB; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Password; +use Illuminate\Support\Str; class UserService { @@ -34,4 +39,25 @@ class UserService User::find($id)->update($data); }, 5); } + + public function inviteUser(array $data): User + { + return DB::transaction(function () use ($data) { + $user = User::create([ + 'name' => $data['name'], + 'email' => $data['email'], + 'role' => UserType::STUDENT->value, + 'status' => $data['status'] ?? 1, + 'password' => Hash::make(Str::random(32)), + ]); + + $token = Password::createToken($user); + + DB::afterCommit(function () use ($user, $token) { + $user->notify(new ResetPasswordNotification($token)); + }); + + return $user; + }, 5); + } } diff --git a/resources/js/pages/dashboard/users/Partials/invite-form.tsx b/resources/js/pages/dashboard/users/Partials/invite-form.tsx new file mode 100644 index 00000000..ee41d9db --- /dev/null +++ b/resources/js/pages/dashboard/users/Partials/invite-form.tsx @@ -0,0 +1,83 @@ +import InputError from '@/components/input-error'; +import LoadingButton from '@/components/loading-button'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { SharedData } from '@/types/global'; +import { useForm, usePage } from '@inertiajs/react'; +import { ReactNode, useState } from 'react'; + +interface Props { + actionComponent: ReactNode; +} + +const InviteForm = ({ actionComponent }: Props) => { + const { props } = usePage(); + const { translate } = props; + const { dashboard, input, common, button } = translate; + const [open, setOpen] = useState(false); + + const { data, post, setData, processing, errors, reset } = useForm({ + name: '', + email: '', + status: 1, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + post(route('users.store'), { + onSuccess: () => { + reset(); + setOpen(false); + }, + }); + }; + + return ( + + {actionComponent} + + + {dashboard.invite_user} +

{dashboard.invite_user_description}

+
+ +
+
+ + setData('name', e.target.value)} /> + +
+ +
+ + setData('email', e.target.value)} /> + +
+ +
+ + + +
+ + + {button.send ?? button.submit} + +
+
+
+ ); +}; + +export default InviteForm; diff --git a/resources/js/pages/dashboard/users/index.tsx b/resources/js/pages/dashboard/users/index.tsx index 1eee91c5..f9301e9b 100644 --- a/resources/js/pages/dashboard/users/index.tsx +++ b/resources/js/pages/dashboard/users/index.tsx @@ -2,6 +2,7 @@ import TableFilter from '@/components/table/table-filter'; import TableFooter from '@/components/table/table-footer'; import TableHeader from '@/components/table/table-header'; import { Card } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table'; import DashboardLayout from '@/layouts/dashboard/layout'; import { SharedData } from '@/types/global'; @@ -9,6 +10,8 @@ import { SortingState, flexRender, getCoreRowModel, getFilteredRowModel, getSort import * as React from 'react'; import { ReactNode } from 'react'; import TableColumn from './Partials/table-columns'; +import InviteForm from './Partials/invite-form'; +import { Plus } from 'lucide-react'; interface Props extends SharedData { users: Pagination; @@ -31,10 +34,20 @@ const Index = (props: Props) => { + + {props.translate.dashboard.invite_user} + + } + /> + } // Icon={} // exportPath={route('users.export')} /> diff --git a/resources/js/types/lang/dashboard.d.ts b/resources/js/types/lang/dashboard.d.ts index e0cdc041..55905115 100644 --- a/resources/js/types/lang/dashboard.d.ts +++ b/resources/js/types/lang/dashboard.d.ts @@ -153,6 +153,8 @@ interface DashboardLang { // Users user_role: string; provide_essential_user_details: string; + invite_user: string; + invite_user_description: string; // Content Management media_library: string; diff --git a/routes/admin.php b/routes/admin.php index a06d9057..1abecf5b 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -29,6 +29,7 @@ use App\Http\Controllers\UsersController; Route::prefix('dashboard')->group(function () { // users Route::resource('users', UsersController::class)->only(['index', 'update']); + Route::post('users', [UsersController::class, 'store'])->name('users.store')->middleware('smtpConfig', 'checkSmtp'); // Category Route::resource('courses/categories', CourseCategoryController::class)->only(['index', 'store', 'destroy'])->names('categories'); diff --git a/storage/app/lang/default/dashboard.php b/storage/app/lang/default/dashboard.php index 72d5095a..d3ed782c 100644 --- a/storage/app/lang/default/dashboard.php +++ b/storage/app/lang/default/dashboard.php @@ -250,6 +250,9 @@ return [ 'user_preferences' => 'User Preferences', 'update_user' => 'Update User', 'select_approval_status' => 'Select the approval status', + 'invite_user' => 'Invite User', + 'invite_user_description' => 'Add a user and email them a password reset link to join.', + 'user_invited' => 'User invited successfully. A password reset link has been sent.', // Course Progress 'course_progress' => 'Course Progress', diff --git a/storage/app/lang/groups/dashboard.php b/storage/app/lang/groups/dashboard.php index 5f86fd3f..e90bdf45 100644 --- a/storage/app/lang/groups/dashboard.php +++ b/storage/app/lang/groups/dashboard.php @@ -393,6 +393,9 @@ return [ 'user_preferences' => 'User Preferences', 'update_user' => 'Update User', 'select_approval_status' => 'Select the approval status', + 'invite_user' => 'Invite User', + 'invite_user_description' => 'Add a user and email them a password reset link to join.', + 'user_invited' => 'User invited successfully. A password reset link has been sent.', ] ],