added add user function
All checks were successful
Build & Push Docker Image / docker (push) Successful in 1m49s

This commit is contained in:
Ahmed Darrazi 2025-12-18 18:10:01 +01:00
parent 731e226a9f
commit 6fba63daf5
9 changed files with 173 additions and 1 deletions

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\StoreUserRequest;
use App\Http\Requests\UpdateUserRequest; use App\Http\Requests\UpdateUserRequest;
use App\Models\User; use App\Models\User;
use App\Services\UserService; use App\Services\UserService;
@ -28,6 +29,16 @@ class UsersController extends Controller
return Inertia::render('dashboard/users/index', compact('users')); 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. * Update the user's account.
*/ */

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:users,email'],
'status' => ['required', 'integer', 'in:0,1'],
];
}
}

View File

@ -2,10 +2,15 @@
namespace App\Services; namespace App\Services;
use App\Enums\UserType;
use App\Models\User; use App\Models\User;
use App\Notifications\ResetPasswordNotification;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
class UserService class UserService
{ {
@ -34,4 +39,25 @@ class UserService
User::find($id)->update($data); User::find($id)->update($data);
}, 5); }, 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);
}
} }

View File

@ -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<SharedData>();
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 (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{actionComponent}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{dashboard.invite_user}</DialogTitle>
<p className="text-muted-foreground text-sm">{dashboard.invite_user_description}</p>
</DialogHeader>
<form onSubmit={handleSubmit} className="mt-4 space-y-4 text-start">
<div>
<Label>{input.name}</Label>
<Input required value={data.name} onChange={(e) => setData('name', e.target.value)} />
<InputError message={errors.name} />
</div>
<div>
<Label>{input.email}</Label>
<Input type="email" required value={data.email} onChange={(e) => setData('email', e.target.value)} />
<InputError message={errors.email} />
</div>
<div>
<Label>{input.status}</Label>
<Select value={data.status === 1 ? 'active' : 'inactive'} onValueChange={(value) => setData('status', value === 'active' ? 1 : 0)}>
<SelectTrigger>
<SelectValue placeholder={dashboard.select_approval_status} />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">{common.active}</SelectItem>
<SelectItem value="inactive">{common.inactive}</SelectItem>
</SelectContent>
</Select>
<InputError message={errors.status} />
</div>
<LoadingButton loading={processing} className="w-full">
{button.send ?? button.submit}
</LoadingButton>
</form>
</DialogContent>
</Dialog>
);
};
export default InviteForm;

View File

@ -2,6 +2,7 @@ import TableFilter from '@/components/table/table-filter';
import TableFooter from '@/components/table/table-footer'; import TableFooter from '@/components/table/table-footer';
import TableHeader from '@/components/table/table-header'; import TableHeader from '@/components/table/table-header';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table'; import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table';
import DashboardLayout from '@/layouts/dashboard/layout'; import DashboardLayout from '@/layouts/dashboard/layout';
import { SharedData } from '@/types/global'; import { SharedData } from '@/types/global';
@ -9,6 +10,8 @@ import { SortingState, flexRender, getCoreRowModel, getFilteredRowModel, getSort
import * as React from 'react'; import * as React from 'react';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import TableColumn from './Partials/table-columns'; import TableColumn from './Partials/table-columns';
import InviteForm from './Partials/invite-form';
import { Plus } from 'lucide-react';
interface Props extends SharedData { interface Props extends SharedData {
users: Pagination<User>; users: Pagination<User>;
@ -31,10 +34,20 @@ const Index = (props: Props) => {
<Card> <Card>
<TableFilter <TableFilter
data={props.users} data={props.users}
title="User List" title={props.translate.dashboard.user_list}
globalSearch={true} globalSearch={true}
tablePageSizes={[10, 15, 20, 25]} tablePageSizes={[10, 15, 20, 25]}
routeName="users.index" routeName="users.index"
component={
<InviteForm
actionComponent={
<Button size="sm">
<Plus className="mr-2 h-4 w-4" />
{props.translate.dashboard.invite_user}
</Button>
}
/>
}
// Icon={<Users className="h-6 w-6 text-primary" />} // Icon={<Users className="h-6 w-6 text-primary" />}
// exportPath={route('users.export')} // exportPath={route('users.export')}
/> />

View File

@ -153,6 +153,8 @@ interface DashboardLang {
// Users // Users
user_role: string; user_role: string;
provide_essential_user_details: string; provide_essential_user_details: string;
invite_user: string;
invite_user_description: string;
// Content Management // Content Management
media_library: string; media_library: string;

View File

@ -29,6 +29,7 @@ use App\Http\Controllers\UsersController;
Route::prefix('dashboard')->group(function () { Route::prefix('dashboard')->group(function () {
// users // users
Route::resource('users', UsersController::class)->only(['index', 'update']); Route::resource('users', UsersController::class)->only(['index', 'update']);
Route::post('users', [UsersController::class, 'store'])->name('users.store')->middleware('smtpConfig', 'checkSmtp');
// Category // Category
Route::resource('courses/categories', CourseCategoryController::class)->only(['index', 'store', 'destroy'])->names('categories'); Route::resource('courses/categories', CourseCategoryController::class)->only(['index', 'store', 'destroy'])->names('categories');

View File

@ -250,6 +250,9 @@ return [
'user_preferences' => 'User Preferences', 'user_preferences' => 'User Preferences',
'update_user' => 'Update User', 'update_user' => 'Update User',
'select_approval_status' => 'Select the approval status', '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' => 'Course Progress', 'course_progress' => 'Course Progress',

View File

@ -393,6 +393,9 @@ return [
'user_preferences' => 'User Preferences', 'user_preferences' => 'User Preferences',
'update_user' => 'Update User', 'update_user' => 'Update User',
'select_approval_status' => 'Select the approval status', '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.',
] ]
], ],