feat: implement user management module with CRUD operations, including user creation, editing, and deletion, along with profile handling
This commit is contained in:
parent
bd80328f65
commit
a4a22e1cc5
110
app/Http/Controllers/Admin/Master/UserController.php
Normal file
110
app/Http/Controllers/Admin/Master/UserController.php
Normal file
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Master;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Master\UserRequest;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function index(): Response
|
||||
{
|
||||
return Inertia::render('admin/master/user/index', [
|
||||
'users' => User::with('profile')->latest()->get(),
|
||||
'defaultPassword' => config('auth.password_default', 'password'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('admin/master/user/create');
|
||||
}
|
||||
|
||||
public function store(UserRequest $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
DB::transaction(function () use ($validated) {
|
||||
$user = User::create([
|
||||
'username' => $validated['username'],
|
||||
'email' => $validated['email'],
|
||||
'password' => Hash::make(config('auth.password_default')),
|
||||
]);
|
||||
|
||||
$user->profile()->create([
|
||||
'nik' => $validated['nik'],
|
||||
'full_name' => $validated['full_name'],
|
||||
'phone_number' => $validated['phone_number'],
|
||||
'address' => $validated['address'],
|
||||
'birth_place' => $validated['birth_place'],
|
||||
'birth_date' => $validated['birth_date'],
|
||||
]);
|
||||
});
|
||||
|
||||
return redirect()->route('user.index')->with('success', 'Data berhasil disimpan');
|
||||
}
|
||||
|
||||
public function edit(User $user): Response
|
||||
{
|
||||
$user->load('profile');
|
||||
|
||||
return Inertia::render('admin/master/user/edit', [
|
||||
'user' => $user,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UserRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
DB::transaction(function () use ($user, $validated) {
|
||||
$user->update([
|
||||
'username' => $validated['username'],
|
||||
'email' => $validated['email'],
|
||||
]);
|
||||
|
||||
$user->profile()->update([
|
||||
'nik' => $validated['nik'],
|
||||
'full_name' => $validated['full_name'],
|
||||
'phone_number' => $validated['phone_number'],
|
||||
'address' => $validated['address'],
|
||||
'birth_place' => $validated['birth_place'],
|
||||
'birth_date' => $validated['birth_date'],
|
||||
]);
|
||||
});
|
||||
|
||||
return redirect()->route('user.index')->with('success', 'Data berhasil diperbarui');
|
||||
}
|
||||
|
||||
public function resetPassword(User $user): RedirectResponse
|
||||
{
|
||||
$user->update([
|
||||
'password' => Hash::make(config('auth.password_default', 'password')),
|
||||
]);
|
||||
|
||||
return redirect()->back()->with('success', 'Password berhasil direset ke default');
|
||||
}
|
||||
|
||||
public function destroy(User $user): RedirectResponse
|
||||
{
|
||||
$user->delete();
|
||||
|
||||
return redirect()->back()->with('success', 'Data berhasil dihapus');
|
||||
}
|
||||
|
||||
public function bulkDestroy(Request $request): RedirectResponse
|
||||
{
|
||||
$ids = $request->input('ids');
|
||||
|
||||
User::whereIn('id', $ids)->delete();
|
||||
|
||||
return redirect()->back()->with('success', 'Data terpilih berhasil dihapus');
|
||||
}
|
||||
}
|
||||
@ -39,7 +39,7 @@ public function share(Request $request): array
|
||||
...parent::share($request),
|
||||
'name' => config('app.name'),
|
||||
'auth' => [
|
||||
'user' => $request->user(),
|
||||
'user' => $request->user()?->load('profile'),
|
||||
],
|
||||
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
|
||||
'flash' => [
|
||||
|
||||
41
app/Http/Requests/Admin/Master/UserRequest.php
Normal file
41
app/Http/Requests/Admin/Master/UserRequest.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Master;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UserRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$userId = $this->route('user')?->id;
|
||||
|
||||
return [
|
||||
'username' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:20',
|
||||
Rule::unique('users', 'username')->ignore($userId),
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:100',
|
||||
Rule::unique('users', 'email')->ignore($userId),
|
||||
],
|
||||
'nik' => ['required', 'max:16', Rule::unique('user_profiles', 'nik')->ignore($userId)],
|
||||
'full_name' => ['required', 'string', 'max:100'],
|
||||
'phone_number' => ['required', 'string', 'max:20'],
|
||||
'address' => ['required', 'string'],
|
||||
'birth_place' => ['required', 'string', 'max:100'],
|
||||
'birth_date' => ['required', 'date'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -3,38 +3,46 @@
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
|
||||
#[Fillable(['name', 'email', 'password'])]
|
||||
#[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])]
|
||||
#[Guarded('id')]
|
||||
#[Hidden(['password'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable, TwoFactorAuthenticatable;
|
||||
use HasFactory, Notifiable, SoftDeletes, TwoFactorAuthenticatable;
|
||||
|
||||
protected $appends = ['name'];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'two_factor_confirmed_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
protected function name(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->profile?->full_name ?? $this->username
|
||||
);
|
||||
}
|
||||
|
||||
public function expenses(): HasMany
|
||||
{
|
||||
return $this->hasMany(Expense::class);
|
||||
}
|
||||
|
||||
public function profile(): HasOne
|
||||
{
|
||||
return $this->hasOne(UserProfile::class);
|
||||
}
|
||||
}
|
||||
|
||||
20
app/Models/UserProfile.php
Normal file
20
app/Models/UserProfile.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class UserProfile extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@ -114,4 +114,5 @@
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
|
||||
'password_default' => env('AUTH_PASSWORD_DEFAULT', 'Minimal8@'),
|
||||
];
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends Factory<User>
|
||||
@ -25,36 +24,26 @@ class UserFactory extends Factory
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'username' => fake()->userName(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
'two_factor_secret' => null,
|
||||
'two_factor_recovery_codes' => null,
|
||||
'two_factor_confirmed_at' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
* Configure the model factory.
|
||||
*/
|
||||
public function unverified(): static
|
||||
public function configure(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model has two-factor authentication configured.
|
||||
*/
|
||||
public function withTwoFactor(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'two_factor_secret' => encrypt('secret'),
|
||||
'two_factor_recovery_codes' => encrypt(json_encode(['recovery-code-1'])),
|
||||
'two_factor_confirmed_at' => now(),
|
||||
]);
|
||||
return $this->afterCreating(function (User $user) {
|
||||
$user->profile()->create([
|
||||
'nik' => fake()->numerify('################'),
|
||||
'full_name' => fake()->name(),
|
||||
'phone_number' => fake()->phoneNumber(),
|
||||
'address' => fake()->address(),
|
||||
'birth_place' => fake()->city(),
|
||||
'birth_date' => fake()->date(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,12 +13,13 @@ public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->string('username', 20);
|
||||
$table->string('email', 100)->unique();
|
||||
$table->string('password', 60);
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_profiles', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('nik', 16);
|
||||
$table->string('full_name', 100);
|
||||
$table->string('phone_number', 20);
|
||||
$table->text('address');
|
||||
$table->string('birth_place', 100);
|
||||
$table->date('birth_date');
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_profiles');
|
||||
}
|
||||
};
|
||||
@ -13,10 +13,19 @@ class UserSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
User::create([
|
||||
'name' => 'Yoga Pangestu',
|
||||
$user = User::create([
|
||||
'username' => 'pangestu',
|
||||
'email' => 'info.pangestuyoga@gmail.com',
|
||||
'password' => Hash::make('Minimal8@'),
|
||||
]);
|
||||
|
||||
$user->profile()->create([
|
||||
'nik' => '3213051307900001',
|
||||
'full_name' => 'Yoga Pangestu',
|
||||
'phone_number' => '082121495806',
|
||||
'address' => 'Jl. Contoh No. 123',
|
||||
'birth_place' => 'Jakarta',
|
||||
'birth_date' => '1990-01-01',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { Boxes, LayoutGrid, List, Wallet } from 'lucide-react';
|
||||
import { Boxes, LayoutGrid, List, User, Wallet } from 'lucide-react';
|
||||
import AppLogo from '@/components/app-logo';
|
||||
import { NavMain } from '@/components/nav-main';
|
||||
import {
|
||||
@ -16,6 +16,7 @@ import category from '@/routes/category';
|
||||
import type { NavItem } from '@/types';
|
||||
import product from '@/routes/product';
|
||||
import expense from '@/routes/expense';
|
||||
import user from '@/routes/user';
|
||||
|
||||
const mainNavItems: NavItem[] = [
|
||||
{
|
||||
@ -26,6 +27,11 @@ const mainNavItems: NavItem[] = [
|
||||
];
|
||||
|
||||
const masterNavItems: NavItem[] = [
|
||||
{
|
||||
title: 'Pegawai',
|
||||
href: user.index().url,
|
||||
icon: User,
|
||||
},
|
||||
{
|
||||
title: 'Kategori',
|
||||
href: category.index().url,
|
||||
|
||||
223
resources/js/pages/admin/master/user/create.tsx
Normal file
223
resources/js/pages/admin/master/user/create.tsx
Normal file
@ -0,0 +1,223 @@
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import { useForm } from '@inertiajs/react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Field } from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import userRoutes from '@/routes/user';
|
||||
import React from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Calendar } from "@/components/ui/calendar"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
|
||||
export default function UserCreate() {
|
||||
const [isCalendarOpen, setIsCalendarOpen] = React.useState(false);
|
||||
|
||||
const { data, setData, post, processing, errors } = useForm({
|
||||
username: '',
|
||||
email: '',
|
||||
nik: '',
|
||||
full_name: '',
|
||||
phone_number: '',
|
||||
address: '',
|
||||
birth_place: '',
|
||||
birth_date: '',
|
||||
});
|
||||
|
||||
const onSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
post(userRoutes.store().url, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
<Head title="Tambah Pegawai" />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Tambah Pegawai</h1>
|
||||
</div>
|
||||
<Link href={userRoutes.index().url}>
|
||||
<Button variant='outline'>
|
||||
Kembali
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<form onSubmit={onSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>Informasi Profil</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<Field>
|
||||
<Label htmlFor="full_name">Nama Lengkap</Label>
|
||||
<Input
|
||||
id="full_name"
|
||||
name="full_name"
|
||||
value={data.full_name}
|
||||
onChange={e => setData('full_name', e.target.value)}
|
||||
autoComplete='off'
|
||||
placeholder='Contoh: John Doe'
|
||||
/>
|
||||
{errors.full_name && <p className="text-xs text-red-500">{errors.full_name}</p>}
|
||||
</Field>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Field>
|
||||
<Label htmlFor="nik">NIK</Label>
|
||||
<Input
|
||||
id="nik"
|
||||
name="nik"
|
||||
value={data.nik}
|
||||
onChange={e => setData('nik', e.target.value)}
|
||||
autoComplete='off'
|
||||
placeholder='Contoh: 3213051307900001'
|
||||
maxLength={16}
|
||||
/>
|
||||
{errors.nik && <p className="text-xs text-red-500">{errors.nik}</p>}
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="phone_number">Nomor Telepon</Label>
|
||||
<Input
|
||||
id="phone_number"
|
||||
name="phone_number"
|
||||
value={data.phone_number}
|
||||
onChange={e => setData('phone_number', e.target.value)}
|
||||
autoComplete='off'
|
||||
placeholder='Contoh: 08123456789'
|
||||
/>
|
||||
{errors.phone_number && <p className="text-xs text-red-500">{errors.phone_number}</p>}
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="birth_place">Tempat Lahir</Label>
|
||||
<Input
|
||||
id="birth_place"
|
||||
name="birth_place"
|
||||
value={data.birth_place}
|
||||
onChange={e => setData('birth_place', e.target.value)}
|
||||
autoComplete='off'
|
||||
placeholder='Contoh: Jakarta'
|
||||
/>
|
||||
{errors.birth_place && <p className="text-xs text-red-500">{errors.birth_place}</p>}
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="birth_date">Tanggal Lahir</Label>
|
||||
<Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="birth_date"
|
||||
className="w-full justify-start font-normal"
|
||||
>
|
||||
{data.birth_date ? (
|
||||
new Intl.DateTimeFormat("id-ID", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}).format(new Date(data.birth_date))
|
||||
) : (
|
||||
<span className="text-muted-foreground">Pilih Tanggal</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={data.birth_date ? new Date(data.birth_date) : undefined}
|
||||
defaultMonth={data.birth_date ? new Date(data.birth_date) : new Date(2000, 0, 1)}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(selectedDate: Date | undefined) => {
|
||||
if (selectedDate) {
|
||||
setData('birth_date', selectedDate.getFullYear() + "-" + String(selectedDate.getMonth() + 1).padStart(2, '0') + "-" + String(selectedDate.getDate()).padStart(2, '0'));
|
||||
} else {
|
||||
setData('birth_date', '');
|
||||
}
|
||||
setIsCalendarOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{errors.birth_date && <p className="text-xs text-red-500">{errors.birth_date}</p>}
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="address">Alamat</Label>
|
||||
<Textarea id='address' name='address' value={data.address} onChange={e => setData('address', e.target.value)} placeholder='Contoh: Kp. Bakan Sampeu' />
|
||||
{errors.address && <p className="text-xs text-red-500">{errors.address}</p>}
|
||||
</Field>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>Kredensial Akun</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<Field>
|
||||
<Label htmlFor="email">Alamat Surel</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={e => setData('email', e.target.value)}
|
||||
autoComplete='off'
|
||||
placeholder='Contoh: john@example.com'
|
||||
/>
|
||||
{errors.email && <p className="text-xs text-red-500">{errors.email}</p>}
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="username">Nama Pengguna</Label>
|
||||
<Input
|
||||
id="username"
|
||||
name="username"
|
||||
value={data.username}
|
||||
onChange={e => setData('username', e.target.value)}
|
||||
autoComplete='off'
|
||||
placeholder='Contoh: johndoe'
|
||||
/>
|
||||
{errors.username && <p className="text-xs text-red-500">{errors.username}</p>}
|
||||
</Field>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex flex-col gap-4 p-4 rounded-xl bg-primary/5 border border-primary/10 shadow-sm">
|
||||
<Button type="submit" className="w-full" disabled={processing}>
|
||||
{processing ? 'Menyimpan...' : 'Simpan'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
UserCreate.layout = {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Master',
|
||||
},
|
||||
],
|
||||
};
|
||||
224
resources/js/pages/admin/master/user/edit.tsx
Normal file
224
resources/js/pages/admin/master/user/edit.tsx
Normal file
@ -0,0 +1,224 @@
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import { useForm } from '@inertiajs/react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Field } from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import userRoutes from '@/routes/user';
|
||||
import React from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Calendar } from "@/components/ui/calendar"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
|
||||
export default function UserEdit({ user }: { user: any }) {
|
||||
const [isCalendarOpen, setIsCalendarOpen] = React.useState(false);
|
||||
|
||||
const { data, setData, post, processing, errors } = useForm({
|
||||
username: user.username || '',
|
||||
email: user.email || '',
|
||||
nik: user.profile?.nik || '',
|
||||
full_name: user.profile?.full_name || '',
|
||||
phone_number: user.profile?.phone_number || '',
|
||||
address: user.profile?.address || '',
|
||||
birth_place: user.profile?.birth_place || '',
|
||||
birth_date: user.profile?.birth_date || '',
|
||||
_method: 'PATCH',
|
||||
});
|
||||
|
||||
const onSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
post(userRoutes.update(user.id).url, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
<Head title={`Ubah Pegawai: ${user.username}`} />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Ubah Pegawai</h1>
|
||||
</div>
|
||||
<Link href={userRoutes.index().url}>
|
||||
<Button variant='outline'>
|
||||
Kembali
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<form onSubmit={onSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>Informasi Profil</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<Field>
|
||||
<Label htmlFor="full_name">Nama Lengkap</Label>
|
||||
<Input
|
||||
id="full_name"
|
||||
name="full_name"
|
||||
value={data.full_name}
|
||||
onChange={e => setData('full_name', e.target.value)}
|
||||
autoComplete='off'
|
||||
placeholder='Contoh: John Doe'
|
||||
/>
|
||||
{errors.full_name && <p className="text-xs text-red-500">{errors.full_name}</p>}
|
||||
</Field>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Field>
|
||||
<Label htmlFor="nik">NIK</Label>
|
||||
<Input
|
||||
id="nik"
|
||||
name="nik"
|
||||
value={data.nik}
|
||||
onChange={e => setData('nik', e.target.value)}
|
||||
autoComplete='off'
|
||||
placeholder='Contoh: 3213051307900001'
|
||||
maxLength={16}
|
||||
/>
|
||||
{errors.nik && <p className="text-xs text-red-500">{errors.nik}</p>}
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="phone_number">Nomor Telepon</Label>
|
||||
<Input
|
||||
id="phone_number"
|
||||
name="phone_number"
|
||||
value={data.phone_number}
|
||||
onChange={e => setData('phone_number', e.target.value)}
|
||||
autoComplete='off'
|
||||
placeholder='Contoh: 08123456789'
|
||||
/>
|
||||
{errors.phone_number && <p className="text-xs text-red-500">{errors.phone_number}</p>}
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="birth_place">Tempat Lahir</Label>
|
||||
<Input
|
||||
id="birth_place"
|
||||
name="birth_place"
|
||||
value={data.birth_place}
|
||||
onChange={e => setData('birth_place', e.target.value)}
|
||||
autoComplete='off'
|
||||
placeholder='Contoh: Jakarta'
|
||||
/>
|
||||
{errors.birth_place && <p className="text-xs text-red-500">{errors.birth_place}</p>}
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="birth_date">Tanggal Lahir</Label>
|
||||
<Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="birth_date"
|
||||
className="w-full justify-start font-normal"
|
||||
>
|
||||
{data.birth_date ? (
|
||||
new Intl.DateTimeFormat("id-ID", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}).format(new Date(data.birth_date))
|
||||
) : (
|
||||
<span className="text-muted-foreground">Pilih Tanggal</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={data.birth_date ? new Date(data.birth_date) : undefined}
|
||||
defaultMonth={data.birth_date ? new Date(data.birth_date) : new Date(2000, 0, 1)}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(selectedDate: Date | undefined) => {
|
||||
if (selectedDate) {
|
||||
setData('birth_date', selectedDate.getFullYear() + "-" + String(selectedDate.getMonth() + 1).padStart(2, '0') + "-" + String(selectedDate.getDate()).padStart(2, '0'));
|
||||
} else {
|
||||
setData('birth_date', '');
|
||||
}
|
||||
setIsCalendarOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{errors.birth_date && <p className="text-xs text-red-500">{errors.birth_date}</p>}
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="address">Alamat</Label>
|
||||
<Textarea id='address' name='address' value={data.address} onChange={e => setData('address', e.target.value)} placeholder='Contoh: Kp. Bakan Sampeu' />
|
||||
{errors.address && <p className="text-xs text-red-500">{errors.address}</p>}
|
||||
</Field>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>Kredensial Akun</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<Field>
|
||||
<Label htmlFor="email">Alamat Surel</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={e => setData('email', e.target.value)}
|
||||
autoComplete='off'
|
||||
placeholder='Contoh: john@example.com'
|
||||
/>
|
||||
{errors.email && <p className="text-xs text-red-500">{errors.email}</p>}
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="username">Nama Pengguna</Label>
|
||||
<Input
|
||||
id="username"
|
||||
name="username"
|
||||
value={data.username}
|
||||
onChange={e => setData('username', e.target.value)}
|
||||
autoComplete='off'
|
||||
placeholder='Contoh: johndoe'
|
||||
/>
|
||||
{errors.username && <p className="text-xs text-red-500">{errors.username}</p>}
|
||||
</Field>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex flex-col gap-4 p-4 rounded-xl bg-primary/5 border border-primary/10 shadow-sm">
|
||||
<Button type="submit" className="w-full" disabled={processing}>
|
||||
{processing ? 'Menyimpan...' : 'Simpan'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
UserEdit.layout = {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Master',
|
||||
},
|
||||
],
|
||||
};
|
||||
273
resources/js/pages/admin/master/user/index.tsx
Normal file
273
resources/js/pages/admin/master/user/index.tsx
Normal file
@ -0,0 +1,273 @@
|
||||
import { Head, router, Link } from '@inertiajs/react';
|
||||
import type { User } from '@/types';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Trash2, Pencil, Plus, KeyRound } from 'lucide-react';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { DataTableColumnHeader } from '@/components/data-table-column-header';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { TooltipContent, TooltipTrigger } from '@radix-ui/react-tooltip';
|
||||
import { Tooltip } from '@/components/ui/tooltip';
|
||||
import userRoutes from '@/routes/user';
|
||||
import { UserInfo } from '@/components/user-info';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogMedia,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
|
||||
export default function UserIndex({ users, defaultPassword }: { users: User[], defaultPassword: string }) {
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||
const [userToDelete, setUserToDelete] = useState<User | null>(null);
|
||||
const [isResetPasswordOpen, setIsResetPasswordOpen] = useState(false);
|
||||
const [userToReset, setUserToReset] = useState<User | null>(null);
|
||||
const [rowsToDelete, setRowsToDelete] = useState<any[]>([]);
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
|
||||
const onResetPassword = (user: User) => {
|
||||
setUserToReset(user);
|
||||
setIsResetPasswordOpen(true);
|
||||
};
|
||||
|
||||
const confirmResetPassword = () => {
|
||||
if (userToReset) {
|
||||
router.patch(userRoutes.resetPassword(userToReset.id).url, {}, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
setIsResetPasswordOpen(false);
|
||||
setUserToReset(null);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = (user: User) => {
|
||||
setUserToDelete(user);
|
||||
setIsDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
if (userToDelete) {
|
||||
router.delete(userRoutes.destroy(userToDelete.id).url, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
setIsDeleteDialogOpen(false);
|
||||
setUserToDelete(null);
|
||||
setRowSelection({});
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const confirmBulkDelete = () => {
|
||||
router.post(userRoutes.bulkDestroy().url, {
|
||||
ids: rowsToDelete.map((row: any) => row.id),
|
||||
_method: 'DELETE'
|
||||
}, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
setIsBulkDeleteDialogOpen(false);
|
||||
setRowsToDelete([]);
|
||||
setRowSelection({});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const columns: ColumnDef<User>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<DataTableColumnHeader column={column} title="User" />
|
||||
)
|
||||
},
|
||||
meta: { title: "User" },
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<UserInfo user={user} showEmail={true} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "username",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<DataTableColumnHeader column={column} title="Nama Pengguna" />
|
||||
)
|
||||
},
|
||||
meta: { title: "Nama Pengguna" },
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.profile?.phone_number,
|
||||
id: "phone_number",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<DataTableColumnHeader column={column} title="Nomor Telepon" />
|
||||
)
|
||||
},
|
||||
meta: { title: "Nomor Telepon" },
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Aksi",
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className='text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:hover:bg-blue-950/20' onClick={() => onResetPassword(user)}>
|
||||
<KeyRound className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Reset Kata Sandi</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link href={userRoutes.edit(user.id).url}>
|
||||
<Button variant="ghost" size="icon" className='text-yellow-600 hover:text-yellow-700 hover:bg-yellow-50 dark:hover:bg-yellow-950/20'>
|
||||
<Pencil className="size-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Ubah</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className='text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/20' onClick={() => onDelete(user)}>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Hapus</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
meta: { title: "Aksi" },
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
<Head title="Pegawai" />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Pegawai</h1>
|
||||
</div>
|
||||
<Link href={userRoutes.create().url}>
|
||||
<Button>
|
||||
Tambah
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm p-5">
|
||||
<CardContent className="p-0">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={users}
|
||||
rowSelection={rowSelection}
|
||||
onRowSelectionChange={setRowSelection}
|
||||
bulkActions={[
|
||||
{
|
||||
label: 'Hapus Terpilih',
|
||||
onClick: (rows) => {
|
||||
setRowsToDelete(rows);
|
||||
setIsBulkDeleteDialogOpen(true);
|
||||
},
|
||||
icon: Trash2,
|
||||
variant: 'destructive'
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<AlertDialogContent size="default">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogMedia className="bg-destructive/10 text-destructive dark:bg-destructive/20 dark:text-destructive">
|
||||
<Trash2 className="size-5" />
|
||||
</AlertDialogMedia>
|
||||
<AlertDialogTitle>Hapus pegawai?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Tindakan ini tidak dapat dibatalkan. Pegawai <strong>{userToDelete?.name}</strong> akan dihapus secara permanen.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel variant="outline">Batal</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={confirmDelete} variant="destructive">Hapus</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog open={isResetPasswordOpen} onOpenChange={setIsResetPasswordOpen}>
|
||||
<AlertDialogContent size="default">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogMedia className="bg-blue-100 text-blue-600 dark:bg-blue-900/20 dark:text-blue-500">
|
||||
<KeyRound className="size-5" />
|
||||
</AlertDialogMedia>
|
||||
<AlertDialogTitle>Reset password?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Tindakan ini akan mereset password <strong>{userToReset?.name}</strong> menjadi <strong>{defaultPassword}</strong>. Pengguna dapat mengubahnya kembali nanti.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel variant="outline">Batal</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={confirmResetPassword} className="bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-600">Reset</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog open={isBulkDeleteDialogOpen} onOpenChange={setIsBulkDeleteDialogOpen}>
|
||||
<AlertDialogContent size="default">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogMedia className="bg-destructive/10 text-destructive dark:bg-destructive/20 dark:text-destructive">
|
||||
<Trash2 className="size-5" />
|
||||
</AlertDialogMedia>
|
||||
<AlertDialogTitle>Hapus {rowsToDelete.length} pegawai?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Tindakan ini tidak dapat dibatalkan. <strong>{rowsToDelete.length}</strong> pegawai yang terpilih akan dihapus secara permanen.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel variant="outline">Batal</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmBulkDelete}
|
||||
variant="destructive"
|
||||
>
|
||||
Hapus
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
UserIndex.layout = {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Master',
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -1,12 +1,27 @@
|
||||
export type UserProfile = {
|
||||
id: number;
|
||||
user_id: number;
|
||||
nik: string;
|
||||
full_name: string;
|
||||
phone_number: string;
|
||||
address: string;
|
||||
birth_place: string;
|
||||
birth_date: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
id: number;
|
||||
name: string;
|
||||
username: string;
|
||||
name: string; // from accessor
|
||||
email: string;
|
||||
avatar?: string;
|
||||
email_verified_at: string | null;
|
||||
two_factor_enabled?: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
profile?: UserProfile;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
use App\Http\Controllers\Admin\Master\CategoryController;
|
||||
use App\Http\Controllers\Admin\Master\ProductController;
|
||||
use App\Http\Controllers\Admin\Master\UserController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
@ -21,5 +22,14 @@
|
||||
Route::delete('product/destroy/{product}', [ProductController::class, 'destroy'])->name('product.destroy');
|
||||
Route::delete('product/bulk-destroy', [ProductController::class, 'bulkDestroy'])->name('product.bulkDestroy');
|
||||
Route::patch('product/toggle-status/{product}', [ProductController::class, 'toggleStatus'])->name('product.toggleStatus');
|
||||
|
||||
Route::get('users', [UserController::class, 'index'])->name('user.index');
|
||||
Route::get('user/create', [UserController::class, 'create'])->name('user.create');
|
||||
Route::post('user/store', [UserController::class, 'store'])->name('user.store');
|
||||
Route::get('user/{user}/edit', [UserController::class, 'edit'])->name('user.edit');
|
||||
Route::patch('user/update/{user}', [UserController::class, 'update'])->name('user.update');
|
||||
Route::patch('user/reset-password/{user}', [UserController::class, 'resetPassword'])->name('user.resetPassword');
|
||||
Route::delete('user/destroy/{user}', [UserController::class, 'destroy'])->name('user.destroy');
|
||||
Route::delete('user/bulk-destroy', [UserController::class, 'bulkDestroy'])->name('user.bulkDestroy');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user