Compare commits

...

11 Commits

Author SHA1 Message Date
AleAlien d73a8e636c
Merge pull request #18 from alealien666/dev
Dev
2025-06-07 18:58:26 +07:00
alealien666 0cc138f0a2 final 2025-06-07 18:52:25 +07:00
alealien666 91dff8b258 semifinal 2025-05-08 18:24:22 +07:00
alealien666 8617fce4aa payment phase 3 2025-05-08 15:45:59 +07:00
alealien666 260942462c payment phase 2 2025-05-08 02:25:41 +07:00
AleAlien 1238ca53fd
Merge pull request #17 from alealien666/manualPay
migrasi react router dom to inertia
2025-03-06 16:01:04 +07:00
alealien666 37a865eef7 migrasi react router dom to inertia 2025-03-06 16:00:47 +07:00
AleAlien a9a07fd834
Merge pull request #16 from alealien666/manualPay
fe
2025-02-28 00:59:33 +07:00
alealien666 a111e526bf fe 2025-02-27 14:28:33 +07:00
AleAlien 2a7731bbd5
Merge pull request #15 from alealien666/manualPay
fe
2025-02-26 03:04:54 +07:00
alealien666 d1e06e99aa fe 2025-02-26 03:04:33 +07:00
128 changed files with 4247 additions and 1771 deletions

View File

@ -1,4 +1,4 @@
APP_NAME=Laravel
APP_NAME=GoGoSantri
APP_ENV=local
APP_KEY=
APP_DEBUG=true

View File

@ -34,6 +34,8 @@ public function store(LoginRequest $request): RedirectResponse
$request->session()->regenerate();
session()->flash('success', 'Login berhasil!');
return redirect()->intended(RouteServiceProvider::HOME);
}
@ -48,6 +50,8 @@ public function destroy(Request $request): RedirectResponse
$request->session()->regenerateToken();
session()->flash('success', 'Logout berhasil!');
return redirect('/');
}
}

View File

@ -22,7 +22,7 @@ class NewPasswordController extends Controller
public function create(Request $request): Response
{
return Inertia::render('Auth/ResetPassword', [
'email' => $request->email,
'nis' => $request->nis,
'token' => $request->route('token'),
]);
}
@ -36,7 +36,7 @@ public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'nis' => 'required|nis',
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
@ -44,7 +44,7 @@ public function store(Request $request): RedirectResponse
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
$request->only('nis', 'password', 'password_confirmation', 'token'),
function ($user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
@ -63,7 +63,7 @@ function ($user) use ($request) {
}
throw ValidationException::withMessages([
'email' => [trans($status)],
'nis' => [trans($status)],
]);
}
}

View File

@ -32,15 +32,16 @@ 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,
// 'name' => 'required|string|max:255',
'nis' => 'required|string|alpha_num|max:255|unique:' . User::class,
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
// 'name' => $request->name,
'nis' => $request->nis,
'password' => Hash::make($request->password),
'level' => 2
]);
event(new Registered($user));

View File

@ -1,65 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\DetailPayment;
use Illuminate\Http\Request;
class DetailPaymentController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(DetailPayment $detailPayment)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(DetailPayment $detailPayment)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, DetailPayment $detailPayment)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(DetailPayment $detailPayment)
{
//
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
use App\Models\User;
use App\Models\Wallet;
use App\Models\DetailPayment;
class HomeController extends Controller
{
public function index()
{
$now = Carbon::now();
$month = $now->month;
$year = $now->year;
$monthlyIncome = DetailPayment::where('status', 'paid')
->where('payment_month', $month)
->where('payment_year', $year)
->whereHas('payments', fn($q) => $q->where('payment_status', 'success'))
->sum(DB::raw('IFNULL(amount,0) + IFNULL(penalty,0)'));
$studentCount = User::where('level', 2)
->where('status_santri', 'aktif')
->count();
$totalBalance = Wallet::sum('saldo');
$paymentTrend = DetailPayment::select('payment_month', 'payment_year')
->selectRaw('SUM(IFNULL(amount,0) + IFNULL(penalty,0)) as total')
->where('status', 'paid')
->whereHas('payments', fn($q) => $q->where('payment_status', 'success'))
->whereBetween(DB::raw("STR_TO_DATE(CONCAT(payment_year,'-',payment_month,'-01'), '%Y-%m-%d')"), [
now()->subMonths(11)->startOfMonth()->toDateString(),
now()->endOfMonth()->toDateString(),
])
->groupBy('payment_year', 'payment_month')
->orderBy('payment_year')
->orderBy('payment_month')
->get();
$labels = [];
$data = [];
foreach ($paymentTrend as $pt) {
$labels[] = Carbon::createFromDate($pt->payment_year, $pt->payment_month, 1)->format('M Y');
$data[] = (int)$pt->total;
}
return Inertia()->render('Dashboard', [
'monthlyIncome' => $monthlyIncome,
'studentCount' => $studentCount,
'totalBalance' => $totalBalance,
'paymentTrend' => [
'labels' => $labels,
'data' => $data,
],
]);
}
}

View File

@ -4,65 +4,161 @@
use App\Models\Payment;
use Illuminate\Http\Request;
use App\Models\Santri;
use App\Models\User;
use App\Models\PaymentType;
use App\Models\DetailPayment;
use Inertia\Inertia;
use App\Services\cekDenda;
use App\Services\GenerateMonthlyBill;
use Illuminate\Support\Facades\DB;
use Exception;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
class PaymentController extends Controller
{
public function indexManualPayment(cekDenda $cekDenda, GenerateMonthlyBill $generateMonthlyBill)
{
$penalty = $cekDenda->applyPenalty();
$bill = $generateMonthlyBill->generateAutoBill();
$paymentType = PaymentType::pluck('payment_type');
$nominal = PaymentType::pluck('nominal');
$paymentTypes = PaymentType::get(['id', 'payment_type', 'nominal']);
$paymentPenalties = DetailPayment::with('paymentType')
->get()
->pluck('penalty', 'type_id');
$santri = Santri::with([
$santri = User::with([
'payments.detailPayments.paymentType',
'user'
])->get();
])->where('level', 2)->paginate(10);
return Inertia::render('list-admin/payment/ManualPayment', [
'santri' => $santri,
'penalty' => $penalty,
'bill' => $bill,
'penalty' => 0,
'fields' => [
'nis' => 'text',
'nama' => 'text',
'status_santri' => 'text',
'role_santri' => 'text',
// 'total_penalty' => 'text',
// 'amount_payment' => 'text',
// 'nominal' => 'text',
// 'payment_type' => 'select',
'nis' => ['type' => 'text', 'readonly' => true],
'nama' => ['type' => 'text', 'readonly' => true],
'status_santri' => ['type' => 'text', 'readonly' => true],
],
'options' => [
'payment_type' => $paymentType,
'payment_nominal' => $nominal,
'payment_type' => $paymentTypes->pluck('payment_type', 'id'),
'payment_nominal' => $paymentTypes->pluck('nominal', 'id'),
'payment_penalty' => $paymentPenalties
]
]);
}
public function manualPayment(Request $request, $id)
public function manualPayment(Request $request, $userId)
{
$request->validate([
''
], [
'amount.required' => 'wajib mengisi nominal pembayaran',
$validator = Validator::make($request->all(), [
'items' => 'required|array',
'items.*.type_id' => 'required|exists:payment_types,id',
'items.*.range' => 'required|integer|min:1',
]);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
DB::beginTransaction();
try {
} catch (Exception $e) {
$paymentTypes = PaymentType::pluck('nominal', 'id');
$user = User::findOrFail($userId);
$existingPayment = Payment::where('user_id', $userId)
->where('payment_status', 'pending')
->lockForUpdate()
->first();
if ($existingPayment) {
$totalAmountExisting = DetailPayment::where('payment_id', $existingPayment->id)->sum('amount');
$hasUnpaid = DetailPayment::where('payment_id', $existingPayment->id)
->where('status', 'unpaid')
->exists();
$existingPayment->update([
'amount_payment' => $totalAmountExisting,
'payment_status' => $hasUnpaid ? 'pending' : 'success',
]);
DB::commit();
return redirect()->back()->with('success', 'Pembayaran yang pending sudah diupdate.');
}
$newPayment = Payment::create([
'payment_status' => 'success',
'amount_payment' => 0,
'transaction_type' => 'payment',
'user_id' => $userId,
'order_id' => 'TRX' . uniqid(),
]);
$totalAmount = 0;
foreach ($request->items as $item) {
$typeId = $item['type_id'];
$range = (int) $item['range'];
$nominal = $paymentTypes[$typeId] ?? 0;
$lastDetail = DetailPayment::whereHas('payments', function ($q) use ($userId) {
$q->where('user_id', $userId);
})
->where('type_id', $typeId)
->orderBy('payment_year', 'desc')
->orderBy('payment_month', 'desc')
->first();
if ($lastDetail) {
$bulan = $lastDetail->payment_month;
$tahun = $lastDetail->payment_year;
} else {
$bulan = now()->month;
$tahun = now()->year;
}
for ($i = 0; $i < $range; $i++) {
if ($i > 0 || $lastDetail) {
$bulan++;
if ($bulan > 12) {
$bulan = 1;
$tahun++;
}
}
DetailPayment::create([
'payment_id' => $newPayment->id,
'payment_month' => $bulan,
'payment_year' => $tahun,
'amount' => $nominal,
'penalty' => 0,
'status' => 'paid',
'type_id' => $typeId,
]);
$totalAmount += $nominal;
}
}
$newPayment->update(['amount_payment' => $totalAmount]);
DB::commit();
return redirect()->back()->with('success', 'Pembayaran baru berhasil dibuat');
} catch (\Exception $e) {
DB::rollBack();
return redirect()->back()->with('error', 'Gagal membuat pembayaran: ' . $e->getMessage());
}
}
public function transaction()
{
$transaction = User::with('payments', 'payments.detailPayments', 'wallet.walletTransactions', 'payments.detailPayments.paymentType')
->where('level', 10)
->paginate(2);
// dd($transaction);
return Inertia::render('list-admin/payment/Transaction', compact('transaction'));
}
}

View File

@ -17,7 +17,8 @@ public function index()
return Inertia::render('list-admin/payment/PaymentType', [
'paymentType' => $paymentType,
'fields' => [
'payment_type' => 'text'
'payment_type' => 'text',
'nominal' => 'number',
]
]);
}
@ -25,17 +26,20 @@ public function index()
public function store(Request $request)
{
$request->validate([
'payment_type' => 'required|string'
'payment_type' => 'required|string',
'nominal' => 'required|numeric'
], [
'payment_type.required' => 'wajib mengisi payment type'
'payment_type.required' => 'wajib mengisi payment type',
'nominal.required' => 'wajib mengisi nominal pembayaran'
]);
try {
PaymentType::create([
'payment_type' => $request->payment_type
'payment_type' => $request->payment_type,
'nominal' => $request->nominal
]);
return redirect()->back()->with('success', 'berhasil insert data');
return redirect()->back()->with('success', 'berhasil insert data tipe pembayaran');
} catch (\Throwable $th) {
return redirect()->back()->with('error', 'Data gagal di tambahkan' . $th->getMessage());
}

View File

@ -3,61 +3,74 @@
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Redirect;
use Inertia\Inertia;
use Inertia\Response;
class ProfileController extends Controller
{
/**
* Display the user's profile form.
*/
public function edit(Request $request): Response
{
return Inertia::render('Profile/Edit', [
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
'status' => session('status'),
]);
// $user = User::all();
return Inertia::render('Profile/Profile');
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return Redirect::route('profile.edit');
}
/**
* Delete the user's account.
*/
public function destroy(Request $request): RedirectResponse
public function updateProfile(Request $request, $id)
{
$request->validate([
'password' => ['required', 'current_password'],
'password' => 'nullable',
'nama' => 'required',
'alamat' => 'required',
'jk' => 'required',
'tanggal_lahir' => 'required|date',
'foto' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048'
], [
'nama.required' => 'wajib mengisi nama santri',
'alamat.required' => 'wajib mengisi alamat santri',
'jk.required' => 'wajib mengisi gender',
'tanggal_lahir.required' => 'wajib mengisi tanggal lahir santri',
'tanggal_lahir.date' => 'tanggal lahir harus dalam format tanggal yang benar',
]);
$user = $request->user();
$santri = User::findOrFail($id);
try {
Auth::logout();
$updateData = [
'nama' => $request->nama,
'alamat' => $request->alamat,
'jk' => $request->jk,
'tanggal_lahir' => $request->tanggal_lahir,
];
$user->delete();
if ($request->hasFile('foto')) {
if ($santri->foto && File::exists(public_path($santri->foto))) {
File::delete(public_path($santri->foto));
}
$request->session()->invalidate();
$request->session()->regenerateToken();
$foto = $request->file('foto');
$fotoName = time() . '_' . $foto->getClientOriginalName();
$foto->move(public_path('fotoSantri'), $fotoName);
$updateData['foto'] = 'fotoSantri/' . $fotoName;
}
if ($request->filled('password')) {
$updateData['password'] = Hash::make($request->password);
}
return Redirect::to('/');
// dd($updateData);
// return $updateData;
$santri->update($updateData);
return redirect()->back()->with('success', 'Data Berhasil Diubah');
} catch (\Throwable $th) {
return redirect()->back()->with('error', 'Gagal memperbarui data: ' . $th->getMessage());
// return $th->getMessage();
}
}
}

View File

@ -2,32 +2,38 @@
namespace App\Http\Controllers;
use App\Models\Santri;
use App\Models\User;
use App\Models\Wallet;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Hash;
class SantriController extends Controller
{
public function index()
{
$santri = Santri::all();
$santri = User::where('level', 2)->paginate(10);
return Inertia::render('list-admin/santri/IndexSantri', [
'santri' => $santri,
'fields' => [
'nama' => 'text',
'nis' => 'text',
'password' => 'password',
'alamat' => 'text',
'no_telp' => 'text',
'status_santri' => 'select',
'role_santri' => 'select',
'jk' => 'select',
'level' => 'select',
'tanggal_lahir' => 'date',
'foto' => 'file'
'foto' => 'file',
],
'options' => [
'status_santri' => ['boyong' => 'Boyong', 'aktif' => 'Aktif'],
'status_santri' => ['lulus' => 'Lulus', 'aktif' => 'Aktif'],
'role_santri' => ['santri' => 'Santri', 'pengurus' => 'Pengurus'],
'jk' => ['laki laki' => 'Laki-Laki', 'perempuan' => 'Perempuan'],
'level' => [1 => 'Admin', 2 => 'User']
],
]);
}
@ -35,21 +41,28 @@ public function index()
public function store(Request $request)
{
$request->validate([
'nis' => 'required',
'password' => 'required',
'level' => 'required',
'nama' => 'required',
'alamat' => 'required',
'status_santri' => 'required',
'role_santri' => 'required',
'jk' => 'required',
'tanggal_lahir' => 'required|date',
'no_telp' => 'required',
'foto' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048'
], [
'nis.required' => 'wajib mengisi nis santri',
'password.required' => 'wajib mengisi password santri',
'level.required' => 'wajib mengisi level santri',
'nama.required' => 'wajib mengisi nama santri',
'alamat.required' => 'wajib mengisi alamat santri',
'status_santri.required' => 'wajib mengisi status santri',
'role.required' => 'wajib mengisi role santri',
'gender.required' => 'wajib mengisi gender',
'ttl.required' => 'wajib mengisi tanggal lahir santri',
'ttl.date' => 'tanggal lahir harus dalam format tanggal yang benar',
'jk.required' => 'wajib mengisi gender',
'no_telp' => 'wajib mengisi no telp',
'tanggal_lahir.required' => 'wajib mengisi tanggal lahir santri',
'tanggal_lahir.date' => 'tanggal lahir harus dalam format tanggal yang benar',
]);
$fotoPath = null;
@ -63,17 +76,22 @@ public function store(Request $request)
}
try {
$santri = Santri::create([
$santri = User::create([
'nis' => $request->nis,
'password' => Hash::make($request->password),
'level' => $request->level,
'nama' => $request->nama,
'alamat' => $request->alamat,
'status_santri' => $request->status_santri,
'role_santri' => $request->role_santri,
'jk' => $request->jk,
'tanggal_lahir' => $request->tanggal_lahir,
'no_telp' => $request->no_telp,
'foto' => $fotoPath
]);
// dd($santri);
$santri->wallet()->create(['saldo' => 0]);
// dd($santri);
return redirect()->back()->with('success', 'Data berhasil ditambahkan');
} catch (\Throwable $th) {
// dd($th->getMessage());
@ -84,30 +102,34 @@ public function store(Request $request)
public function update(Request $request, $id)
{
$request->validate([
'nis' => 'required',
'password' => 'nullable',
'level' => 'required',
'nama' => 'required',
'alamat' => 'required',
'status_santri' => 'required',
'role_santri' => 'required',
'jk' => 'required',
'tanggal_lahir' => 'required|date',
'foto' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048'
], [
'nis.required' => 'wajib mengisi nis santri',
'level.required' => 'wajib mengisi level santri',
'nama.required' => 'wajib mengisi nama santri',
'alamat.required' => 'wajib mengisi alamat santri',
'status_santri.required' => 'wajib mengisi status santri',
'role_santri.required' => 'wajib mengisi role santri',
'jk.required' => 'wajib mengisi jenis kelamin',
'jk.required' => 'wajib mengisi gender',
'tanggal_lahir.required' => 'wajib mengisi tanggal lahir santri',
'tanggal_lahir.date' => 'tanggal lahir harus dalam format tanggal yang benar',
]);
try {
$santri = Santri::findOrFail($id);
$santri = User::findOrFail($id);
$updateData = [
'nis' => $request->nis,
'level' => $request->level,
'nama' => $request->nama,
'alamat' => $request->alamat,
'status_santri' => $request->status_santri,
'role_santri' => $request->role_santri,
'jk' => $request->jk,
'tanggal_lahir' => $request->tanggal_lahir,
];
@ -122,18 +144,24 @@ public function update(Request $request, $id)
$foto->move(public_path('fotoSantri'), $fotoName);
$updateData['foto'] = 'fotoSantri/' . $fotoName;
}
if ($request->filled('password')) {
$updateData['password'] = Hash::make($request->password);
}
// return $updateData;
$updateStatus = $santri->update($updateData);
return redirect()->back()->with('success', 'Data Berhasil Diubah');
} catch (\Throwable $th) {
return redirect()->back()->with('error', 'Gagal memperbarui data: ' . $th->getMessage());
// return $th->getMessage();
}
}
public function destroy(Santri $santri, $id)
public function destroy($id)
{
$santri = Santri::findOrFail($id);
$santri = User::findOrFail($id);
$santri->delete();
File::delete('fotoSantri/' . basename($santri->foto));

View File

@ -2,64 +2,18 @@
namespace App\Http\Controllers;
use App\Models\Wallet;
use App\Models\User;
use Illuminate\Http\Request;
use Inertia\Inertia;
class WalletController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
public function walletUser()
{
//
}
$wallet = User::with('wallet')
->where('level', 2)
->paginate(10);
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(Wallet $wallet)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Wallet $wallet)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Wallet $wallet)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Wallet $wallet)
{
//
return Inertia::render('list-admin/payment/WalletUser', compact('wallet'));
}
}

View File

@ -1,65 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\WalletTransaction;
use Illuminate\Http\Request;
class WalletTransactionController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(WalletTransaction $walletTransaction)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(WalletTransaction $walletTransaction)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, WalletTransaction $walletTransaction)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(WalletTransaction $walletTransaction)
{
//
}
}

View File

@ -34,6 +34,10 @@ public function share(Request $request): array
'auth' => [
'user' => $request->user(),
],
'flash' => [
'success' => fn() => $request->session()->get('success'),
'error' => fn() => $request->session()->get('error'),
],
];
}
}

View File

@ -27,7 +27,7 @@ public function authorize(): bool
public function rules(): array
{
return [
'email' => ['required', 'string', 'email'],
'nis' => ['required', 'string'],
'password' => ['required', 'string'],
];
}
@ -41,11 +41,11 @@ public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
if (! Auth::attempt($this->only('nis', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
'nis' => trans('auth.failed'),
]);
}
@ -68,7 +68,7 @@ public function ensureIsNotRateLimited(): void
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'nis' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
@ -80,6 +80,6 @@ public function ensureIsNotRateLimited(): void
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
return Str::transliterate(Str::lower($this->string('nis')) . '|' . $this->ip());
}
}

View File

@ -11,9 +11,9 @@ class Payment extends Model
protected $guarded = ['id'];
public function santri()
public function user()
{
return $this->belongsTo(Santri::class, 'santri_id', 'id');
return $this->belongsTo(User::class, 'user_id', 'id');
}
public function wallet()

View File

@ -1,29 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Santri extends Model
{
use HasFactory;
protected $guarded = ['id'];
protected $table = 'santris';
public function user()
{
return $this->hasOne(User::class, 'santri_id', 'id');
}
public function payments()
{
return $this->hasMany(Payment::class, 'santri_id', 'id');
}
public function wallet()
{
return $this->hasOne(Wallet::class, 'santri_id', 'id');
}
}

View File

@ -17,10 +17,8 @@ class User extends Authenticatable
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
protected $guarded = [
'id'
];
/**
@ -38,12 +36,14 @@ class User extends Authenticatable
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
public function santri()
public function payments()
{
return $this->belongsTo(Santri::class, 'santri_id', 'id');
return $this->hasMany(Payment::class, 'user_id', 'id');
}
public function wallet()
{
return $this->hasOne(Wallet::class, 'user_id', 'id');
}
}

View File

@ -16,9 +16,9 @@ public function payments()
return $this->hasMany(Payment::class, 'wallet_id', 'id');
}
public function santri()
public function user()
{
return $this->belongsTo(Santri::class, 'santri_id', 'id');
return $this->belongsTo(User::class, 'user_id', 'id');
}
public function walletTransactions()

View File

@ -3,6 +3,7 @@
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Inertia\Inertia;
class AppServiceProvider extends ServiceProvider
{
@ -19,6 +20,11 @@ public function register(): void
*/
public function boot(): void
{
//
Inertia::share([
'flash' => [
'success' => session('success'),
'error' => session('error'),
],
]);
}
}

View File

@ -1,34 +0,0 @@
<?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('santris', function (Blueprint $table) {
$table->id();
$table->string('nama');
$table->string('alamat');
$table->enum('status_santri', ['boyong', 'aktif']);
$table->enum('role_santri', ['santri', 'pengurus']);
$table->enum('jk', ['laki laki', 'perempuan']);
$table->date('tanggal_lahir');
$table->string('foto')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('santris');
}
};

View File

@ -16,7 +16,13 @@ public function up(): void
$table->string('nis');
$table->string('password');
$table->string('level');
$table->foreignId('santri_id')->constrained('santris')->onDelete('cascade');
$table->string('nama');
$table->string('alamat');
$table->bigInteger('no_telp');
$table->enum('status_santri', ['lulus', 'aktif']);
$table->enum('jk', ['laki laki', 'perempuan']);
$table->date('tanggal_lahir');
$table->string('foto')->nullable();
$table->rememberToken();
$table->timestamps();
});

View File

@ -14,7 +14,7 @@ public function up(): void
Schema::create('payment_types', function (Blueprint $table) {
$table->id();
$table->string('payment_type');
$table->float('nominal');
$table->decimal('nominal');
$table->timestamps();
});
}

View File

@ -13,8 +13,8 @@ public function up(): void
{
Schema::create('wallets', function (Blueprint $table) {
$table->id();
$table->foreignId('santri_id')->constrained('santris')->onDelete('cascade');
$table->float('saldo');
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->decimal('saldo');
$table->timestamps();
});
}

View File

@ -13,9 +13,10 @@ public function up(): void
{
Schema::create('wallet_transactions', function (Blueprint $table) {
$table->id();
$table->string('order_id')->unique();
$table->foreignId('wallet_id')->constrained('wallets')->onDelete('cascade');
$table->enum('transaction_type', ['topup', 'payment']);
$table->float('amount');
$table->decimal('amount');
$table->string('description');
$table->enum('status', ['pending', 'success', 'failed']);
$table->timestamps();

View File

@ -13,12 +13,15 @@ public function up(): void
{
Schema::create('payments', function (Blueprint $table) {
$table->id();
$table->string('order_id')->unique()->nullable();
$table->enum('payment_status', ['pending', 'failed', 'success']);
$table->float('amount_payment')->nullable();
$table->enum('transaction_type', ['topup', 'payment']);
$table->decimal('amount_payment')->nullable();
$table->String('bank')->nullable();
$table->string('no_va')->nullable();
$table->dateTime('expired_at')->nullable();
$table->foreignId('santri_id')->constrained('santris')->onDelete('cascade');
$table->string('snap_token')->nullable();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->foreignId('wallet_id')->nullable()->constrained('wallets')->onDelete('cascade');
$table->timestamps();
});

View File

@ -15,8 +15,8 @@ public function up(): void
$table->id();
$table->foreignId('payment_id')->constrained('payments')->onDelete('cascade');
$table->enum('status', ['paid', 'unpaid']);
$table->float('amount')->nullable();
$table->float('penalty')->nullable();
$table->decimal('amount')->nullable();
$table->decimal('penalty')->nullable();
$table->integer('payment_month');
$table->integer('payment_year');
$table->foreignId('type_id')->constrained('payment_types')->onDelete('cascade');

View File

@ -4,6 +4,8 @@
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
class DatabaseSeeder extends Seeder
{
@ -12,11 +14,31 @@ class DatabaseSeeder extends Seeder
*/
public function run(): void
{
// \App\Models\User::factory(10)->create();
// \App\Models\User::factory()->create([
// 'name' => 'Test User',
// 'email' => 'test@example.com',
// ]);
User::insert([
[
'nis' => '1234567890',
'password' => Hash::make('password123'),
'level' => 1,
'nama' => 'Admin',
'alamat' => 'Jl. MH. Thamrin No. 10, Ajong',
'status_santri' => 'aktif',
'jk' => 'laki laki',
'tanggal_lahir' => '2005-08-15',
'foto' => null,
'no_telp' => '80989080980'
],
[
'nis' => '0987654321',
'password' => Hash::make('pitik123'),
'level' => 2,
'nama' => 'Ahmad Kasim',
'alamat' => 'Jl. Pesantren No. 10, Jakarta',
'status_santri' => 'aktif',
'jk' => 'laki laki',
'tanggal_lahir' => '2003-08-15',
'foto' => null,
'no_telp' => '80989080980'
]
],);
}
}

401
package-lock.json generated
View File

@ -5,11 +5,19 @@
"packages": {
"": {
"dependencies": {
"@inertiajs/inertia": "^0.11.1"
"@heroicons/react": "^2.2.0",
"@inertiajs/inertia": "^0.11.1",
"@inertiajs/react": "^2.0.4",
"@reduxjs/toolkit": "^2.6.0",
"react-chartjs-2": "^5.3.0",
"react-redux": "^9.2.0",
"react-tailwindcss-datepicker": "^2.0.0",
"sweetalert2": "^11.21.0",
"theme-change": "^2.5.0",
"web-vitals": "^4.2.4"
},
"devDependencies": {
"@headlessui/react": "^1.4.2",
"@inertiajs/react": "^1.3.0",
"@tailwindcss/forms": "^0.5.3",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.12",
@ -18,7 +26,6 @@
"laravel-vite-plugin": "^0.7.2",
"postcss": "^8.4.31",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.2.1",
"vite": "^4.0.0"
}
@ -63,30 +70,30 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz",
"integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==",
"version": "7.26.8",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz",
"integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz",
"integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz",
"integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.2",
"@babel/generator": "^7.26.5",
"@babel/generator": "^7.26.9",
"@babel/helper-compilation-targets": "^7.26.5",
"@babel/helper-module-transforms": "^7.26.0",
"@babel/helpers": "^7.26.7",
"@babel/parser": "^7.26.7",
"@babel/template": "^7.25.9",
"@babel/traverse": "^7.26.7",
"@babel/types": "^7.26.7",
"@babel/helpers": "^7.26.9",
"@babel/parser": "^7.26.9",
"@babel/template": "^7.26.9",
"@babel/traverse": "^7.26.9",
"@babel/types": "^7.26.9",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@ -102,13 +109,13 @@
}
},
"node_modules/@babel/generator": {
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
"integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz",
"integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.26.5",
"@babel/types": "^7.26.5",
"@babel/parser": "^7.26.9",
"@babel/types": "^7.26.9",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^3.0.2"
@ -200,25 +207,25 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz",
"integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz",
"integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==",
"dev": true,
"dependencies": {
"@babel/template": "^7.25.9",
"@babel/types": "^7.26.7"
"@babel/template": "^7.26.9",
"@babel/types": "^7.26.9"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
"integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz",
"integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==",
"dev": true,
"dependencies": {
"@babel/types": "^7.26.7"
"@babel/types": "^7.26.9"
},
"bin": {
"parser": "bin/babel-parser.js"
@ -258,30 +265,30 @@
}
},
"node_modules/@babel/template": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.25.9",
"@babel/parser": "^7.25.9",
"@babel/types": "^7.25.9"
"@babel/code-frame": "^7.26.2",
"@babel/parser": "^7.26.9",
"@babel/types": "^7.26.9"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz",
"integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz",
"integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.26.2",
"@babel/generator": "^7.26.5",
"@babel/parser": "^7.26.7",
"@babel/template": "^7.25.9",
"@babel/types": "^7.26.7",
"@babel/generator": "^7.26.9",
"@babel/parser": "^7.26.9",
"@babel/template": "^7.26.9",
"@babel/types": "^7.26.9",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
@ -290,9 +297,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
"integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz",
"integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
@ -671,15 +678,21 @@
"react-dom": "^16 || ^17 || ^18"
}
},
"node_modules/@heroicons/react": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
"integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
"peerDependencies": {
"react": ">= 16 || ^19.0.0-rc"
}
},
"node_modules/@inertiajs/core": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-1.3.0.tgz",
"integrity": "sha512-TJ8R1eUYY473m9DaKlCPRdHTdznFWTDuy5VvEzXg3t/hohbDQedLj46yn/uAqziJPEUZJrSftZzPI2NMzL9tQA==",
"dev": true,
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-2.0.4.tgz",
"integrity": "sha512-gCUqpwBRYOhz0hwBDWca2lkk+Mc+36GvbRoE0rEvYFpzQAMMP0xFhH9h8hr7VWTn+vVOZRuDvakI+4cazwtvCg==",
"dependencies": {
"axios": "^1.6.0",
"deepmerge": "^4.0.0",
"nprogress": "^0.2.0",
"qs": "^6.9.0"
}
},
@ -702,12 +715,11 @@
}
},
"node_modules/@inertiajs/react": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@inertiajs/react/-/react-1.3.0.tgz",
"integrity": "sha512-K+PF23xP6jjMkubs8PbxT1MroSDdH1z3VTEGbO3685Xyf0QNwoNIF95hnyqJxlWaeG4fB0GAag40gh04fefRUA==",
"dev": true,
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@inertiajs/react/-/react-2.0.4.tgz",
"integrity": "sha512-syPqZNVU5v0DB3VHCm9aVQafJ9kgkxtC5lfc4WOTBxtUjZjbJYDwt5d0yLOhyfU4S7d9CR0dhlkkEt1DsedD3Q==",
"dependencies": {
"@inertiajs/core": "1.3.0",
"@inertiajs/core": "2.0.4",
"lodash.isequal": "^4.5.0"
},
"peerDependencies": {
@ -779,6 +791,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"peer": true
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -824,6 +842,29 @@
"node": ">=14"
}
},
"node_modules/@reduxjs/toolkit": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.6.0.tgz",
"integrity": "sha512-mWJCYpewLRyTuuzRSEC/IwIBBkYg2dKtQas8mty5MaV2iXzcmicS3gW554FDeOvLnY3x13NIk8MB1e8wHO7rqQ==",
"dependencies": {
"immer": "^10.0.3",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@tailwindcss/forms": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
@ -837,12 +878,12 @@
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.3.tgz",
"integrity": "sha512-vCU+OTylXN3hdC8RKg68tPlBPjjxtzon7Ys46MgrSLE+JhSjSTPvoQifV6DQJeJmA8Q3KT6CphJbejupx85vFw==",
"version": "3.13.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.2.tgz",
"integrity": "sha512-LceSUgABBKF6HSsHK2ZqHzQ37IKV/jlaWbHm+NyTa3/WNb/JZVcThDuTainf+PixltOOcFCYXwxbLpOX9sCx+g==",
"dev": true,
"dependencies": {
"@tanstack/virtual-core": "3.11.3"
"@tanstack/virtual-core": "3.13.2"
},
"funding": {
"type": "github",
@ -854,9 +895,9 @@
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.3.tgz",
"integrity": "sha512-v2mrNSnMwnPJtcVqNvV0c5roGCBqeogN8jDtgtuHCphdwBasOZ17x8UV8qpHUh+u0MLfX43c0uUHKje0s+Zb0w==",
"version": "3.13.2",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.2.tgz",
"integrity": "sha512-Qzz4EgzMbO5gKrmqUondCjiHcuu4B1ftHb0pjCut661lXZdGoHeze9f/M8iwsK1t5LGR6aNuNGU7mxkowaW6RQ==",
"dev": true,
"funding": {
"type": "github",
@ -904,6 +945,11 @@
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
},
"node_modules/@vitejs/plugin-react": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
@ -975,8 +1021,7 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/autoprefixer": {
"version": "10.4.20",
@ -1019,7 +1064,6 @@
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@ -1098,9 +1142,9 @@
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
@ -1134,9 +1178,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001696",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz",
"integrity": "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==",
"version": "1.0.30001700",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz",
"integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==",
"dev": true,
"funding": [
{
@ -1153,6 +1197,18 @@
}
]
},
"node_modules/chart.js": {
"version": "4.4.8",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz",
"integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==",
"peer": true,
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@ -1217,7 +1273,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@ -1304,6 +1359,12 @@
"url": "https://opencollective.com/daisyui"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"peer": true
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@ -1333,7 +1394,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
@ -1370,9 +1430,9 @@
"dev": true
},
"node_modules/electron-to-chromium": {
"version": "1.5.88",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.88.tgz",
"integrity": "sha512-K3C2qf1o+bGzbilTDCTBhTQcMS9KW60yTAaTeeXsfvQuTDDwlokLam/AdqlqcSy9u4UainDgsHV23ksXAOgamw==",
"version": "1.5.104",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.104.tgz",
"integrity": "sha512-Us9M2L4cO/zMBqVkJtnj353nQhMju9slHm62NprKTmdF3HH8wYOtNvDFq/JB2+ZRoGLzdvYDiATlMHs98XBM1g==",
"dev": true
},
"node_modules/emoji-regex": {
@ -1408,6 +1468,20 @@
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
@ -1489,9 +1563,9 @@
"dev": true
},
"node_modules/fastq": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
"integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==",
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz",
"integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==",
"dev": true,
"dependencies": {
"reusify": "^1.0.4"
@ -1529,12 +1603,12 @@
}
},
"node_modules/foreground-child": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
"integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.0",
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
@ -1545,13 +1619,13 @@
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"dev": true,
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
@ -1603,16 +1677,16 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
"integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.0",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
@ -1700,6 +1774,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@ -1711,6 +1799,15 @@
"node": ">= 0.4"
}
},
"node_modules/immer": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -1810,8 +1907,7 @@
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/jsesc": {
"version": "3.1.0",
@ -1875,14 +1971,12 @@
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"dev": true
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead."
},
"node_modules/loose-envify": {
"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,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@ -1933,7 +2027,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
@ -1942,7 +2035,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
@ -2042,12 +2134,6 @@
"node": ">=0.10.0"
}
},
"node_modules/nprogress": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
"integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==",
"dev": true
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -2067,9 +2153,9 @@
}
},
"node_modules/object-inspect": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
"integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"engines": {
"node": ">= 0.4"
},
@ -2157,9 +2243,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true,
"funding": [
{
@ -2302,8 +2388,7 @@
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/qs": {
"version": "6.14.0",
@ -2343,7 +2428,6 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dev": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@ -2351,11 +2435,21 @@
"node": ">=0.10.0"
}
},
"node_modules/react-chartjs-2": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
"integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
"peerDependencies": {
"chart.js": "^4.1.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"dev": true,
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@ -2364,6 +2458,28 @@
"react": "^18.3.1"
}
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@ -2373,6 +2489,15 @@
"node": ">=0.10.0"
}
},
"node_modules/react-tailwindcss-datepicker": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-tailwindcss-datepicker/-/react-tailwindcss-datepicker-2.0.0.tgz",
"integrity": "sha512-HADddzZjeOIMxkKkueAyQmiAExLXYcwgPG0Qv1oP9cjCx4/jrhbbOAbazgjJmxfhGxUw7e0Goqs+DY3htKlhAA==",
"peerDependencies": {
"dayjs": "^1.11.12",
"react": "^17.0.2 || ^18.2.0 || ^19.0.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -2394,6 +2519,24 @@
"node": ">=8.10.0"
}
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@ -2468,6 +2611,7 @@
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dev": true,
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
@ -2721,6 +2865,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sweetalert2": {
"version": "11.21.0",
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.21.0.tgz",
"integrity": "sha512-fiEK7SqRY/QD/wC2uqEHlfYGZ7qe2UcyQbJpbpj4YRVqplBgcI+euPZLZL+evLINcvbtXmL1SFUdZHKqBHGAAQ==",
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/limonte"
}
},
"node_modules/tailwindcss": {
"version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
@ -2758,6 +2911,11 @@
"node": ">=14.0.0"
}
},
"node_modules/theme-change": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/theme-change/-/theme-change-2.5.0.tgz",
"integrity": "sha512-B/UdsgdHAGhSKHTAQnxg/etN0RaMDpehuJmZIjLMDVJ6DGIliRHGD6pODi1CXLQAN9GV0GSyB3G6yCuK05PkPQ=="
},
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@ -2827,6 +2985,14 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -2898,6 +3064,11 @@
"picomatch": "^2.3.1"
}
},
"node_modules/web-vitals": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -6,7 +6,6 @@
},
"devDependencies": {
"@headlessui/react": "^1.4.2",
"@inertiajs/react": "^1.3.0",
"@tailwindcss/forms": "^0.5.3",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.12",
@ -15,11 +14,19 @@
"laravel-vite-plugin": "^0.7.2",
"postcss": "^8.4.31",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.2.1",
"vite": "^4.0.0"
},
"dependencies": {
"@inertiajs/inertia": "^0.11.1"
"@heroicons/react": "^2.2.0",
"@inertiajs/inertia": "^0.11.1",
"@inertiajs/react": "^2.0.4",
"@reduxjs/toolkit": "^2.6.0",
"react-chartjs-2": "^5.3.0",
"react-redux": "^9.2.0",
"react-tailwindcss-datepicker": "^2.0.0",
"sweetalert2": "^11.21.0",
"theme-change": "^2.5.0",
"web-vitals": "^4.2.4"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -0,0 +1,25 @@
{
"short_name": "DashWind",
"name": "DashWind",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/assets/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

View File

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

View File

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

14
public/index.html Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React Test</title>
</head>
<body>
<div id="root">Loading...</div>
</body>
</html>

View File

@ -0,0 +1,44 @@
const moment = require("moment");
module.exports = Object.freeze({
CALENDAR_INITIAL_EVENTS : [
{title : "Product call", theme : "GREEN", startTime : moment().add(-12, 'd').startOf('day'), endTime : moment().add(-12, 'd').endOf('day')},
{title : "Meeting with tech team", theme : "PINK", startTime : moment().add(-8, 'd').startOf('day'), endTime : moment().add(-8, 'd').endOf('day')},
{title : "Meeting with Cristina", theme : "PURPLE", startTime : moment().add(-2, 'd').startOf('day'), endTime : moment().add(-2, 'd').endOf('day')},
{title : "Meeting with Alex", theme : "BLUE", startTime : moment().startOf('day'), endTime : moment().endOf('day')},
{title : "Product Call", theme : "GREEN", startTime : moment().startOf('day'), endTime : moment().endOf('day')},
{title : "Client Meeting", theme : "PURPLE", startTime : moment().startOf('day'), endTime : moment().endOf('day')},
{title : "Client Meeting", theme : "ORANGE", startTime : moment().add(3, 'd').startOf('day'), endTime : moment().add(3, 'd').endOf('day')},
{title : "Product meeting", theme : "PINK", startTime : moment().add(5, 'd').startOf('day'), endTime : moment().add(5, 'd').endOf('day')},
{title : "Sales Meeting", theme : "GREEN", startTime : moment().add(8, 'd').startOf('day'), endTime : moment().add(8, 'd').endOf('day')},
{title : "Product Meeting", theme : "ORANGE", startTime : moment().add(8, 'd').startOf('day'), endTime : moment().add(8, 'd').endOf('day')},
{title : "Marketing Meeting", theme : "PINK", startTime : moment().add(8, 'd').startOf('day'), endTime : moment().add(8, 'd').endOf('day')},
{title : "Client Meeting", theme : "GREEN", startTime : moment().add(8, 'd').startOf('day'), endTime : moment().add(8, 'd').endOf('day')},
{title : "Sales meeting", theme : "BLUE", startTime : moment().add(12, 'd').startOf('day'), endTime : moment().add(12, 'd').endOf('day')},
{title : "Client meeting", theme : "PURPLE", startTime : moment().add(16, 'd').startOf('day'), endTime : moment().add(16, 'd').endOf('day')},
],
RECENT_TRANSACTIONS : [
{name : "Alex", avatar : "https://reqres.in/img/faces/1-image.jpg", email : "alex@dashwind.com", location : "Paris", amount : 100, date : moment().endOf('day')},
{name : "Ereena", avatar : "https://reqres.in/img/faces/2-image.jpg", email : "ereena@dashwind.com", location : "London", amount : 190, date : moment().add(-1, 'd').endOf('day')},
{name : "John", avatar : "https://reqres.in/img/faces/3-image.jpg", email : "jhon@dashwind.com", location : "Canada", amount : 112, date : moment().add(-1, 'd').endOf('day')},
{name : "Matrix", avatar : "https://reqres.in/img/faces/4-image.jpg", email : "matrix@dashwind.com", location : "Peru", amount : 111, date : moment().add(-1, 'd').endOf('day')},
{name : "Virat", avatar : "https://reqres.in/img/faces/5-image.jpg", email : "virat@dashwind.com", location : "London", amount : 190, date : moment().add(-2, 'd').endOf('day')},
{name : "Miya", avatar : "https://reqres.in/img/faces/6-image.jpg", email : "miya@dashwind.com", location : "Paris", amount : 230, date : moment().add(-2, 'd').endOf('day')},
{name : "Virat", avatar : "https://reqres.in/img/faces/3-image.jpg", email : "virat@dashwind.com", location : "Canada", amount : 331, date : moment().add(-2, 'd').endOf('day')},
{name : "Matrix", avatar : "https://reqres.in/img/faces/1-image.jpg", email : "matrix@dashwind.com", location : "London", amount : 581, date : moment().add(-2, 'd').endOf('day')},
{name : "Ereena", avatar : "https://reqres.in/img/faces/3-image.jpg", email : "ereena@dashwind.com", location : "Tokyo", amount : 151, date : moment().add(-2, 'd').endOf('day')},
{name : "John", avatar : "https://reqres.in/img/faces/2-image.jpg", email : "jhon@dashwind.com", location : "Paris", amount : 91, date : moment().add(-2, 'd').endOf('day')},
{name : "Virat", avatar : "https://reqres.in/img/faces/3-image.jpg", email : "virat@dashwind.com", location : "Canada", amount : 161, date : moment().add(-3, 'd').endOf('day')},
{name : "Matrix", avatar : "https://reqres.in/img/faces/4-image.jpg", email : "matrix@dashwind.com", location : "US", amount : 121, date : moment().add(-3, 'd').endOf('day')},
{name : "Ereena", avatar : "https://reqres.in/img/faces/6-image.jpg", email : "jhon@dashwind.com", location : "Tokyo", amount : 713, date : moment().add(-3, 'd').endOf('day')},
{name : "John", avatar : "https://reqres.in/img/faces/2-image.jpg", email : "ereena@dashwind.com", location : "London", amount : 217, date : moment().add(-3, 'd').endOf('day')},
{name : "Virat", avatar : "https://reqres.in/img/faces/3-image.jpg", email : "virat@dashwind.com", location : "Paris", amount : 117, date : moment().add(-3, 'd').endOf('day')},
{name : "Miya", avatar : "https://reqres.in/img/faces/7-image.jpg", email : "jhon@dashwind.com", location : "Canada", amount : 612, date : moment().add(-3, 'd').endOf('day')},
{name : "Matrix", avatar : "https://reqres.in/img/faces/3-image.jpg", email : "matrix@dashwind.com", location : "London", amount : 631, date : moment().add(-3, 'd').endOf('day')},
{name : "Virat", avatar : "https://reqres.in/img/faces/2-image.jpg", email : "ereena@dashwind.com", location : "Tokyo", amount : 151, date : moment().add(-3, 'd').endOf('day')},
{name : "Ereena", avatar : "https://reqres.in/img/faces/3-image.jpg", email : "virat@dashwind.com", location : "Paris", amount : 617, date : moment().add(-3, 'd').endOf('day')},
]
});

View File

@ -0,0 +1,15 @@
export const MODAL_BODY_TYPES = Object.freeze({
USER_DETAIL: "USER_DETAIL",
LEAD_ADD_NEW: "LEAD_ADD_NEW",
CONFIRMATION: "CONFIRMATION",
DEFAULT: "",
});
export const RIGHT_DRAWER_TYPES = Object.freeze({
NOTIFICATION: "NOTIFICATION",
CALENDAR_EVENTS: "CALENDAR_EVENTS",
});
export const CONFIRMATION_MODAL_CLOSE_TYPES = Object.freeze({
LEAD_DELETE: "LEAD_DELETE",
});

38
resources/js/App.css Normal file
View File

@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,37 @@
import axios from "axios"
const checkAuth = () => {
/* Getting token value stored in localstorage, if token is not present we will open login page
for all internal dashboard routes */
const TOKEN = localStorage.getItem("token")
const PUBLIC_ROUTES = ["login", "forgot-password", "register", "documentation"]
const isPublicPage = PUBLIC_ROUTES.some(r => window.location.href.includes(r))
if (!TOKEN && !isPublicPage) {
window.location.href = '/login'
return;
} else {
axios.defaults.headers.common['Authorization'] = `Bearer ${TOKEN}`
axios.interceptors.request.use(function (config) {
// UPDATE: Add this code to show global loading indicator
document.body.classList.add('loading-indicator');
return config
}, function (error) {
return Promise.reject(error);
});
axios.interceptors.response.use(function (response) {
// UPDATE: Add this code to hide global loading indicator
document.body.classList.remove('loading-indicator');
return response;
}, function (error) {
document.body.classList.remove('loading-indicator');
return Promise.reject(error);
});
return TOKEN
}
}
export default checkAuth

View File

@ -0,0 +1,27 @@
import axios from "axios"
const initializeApp = () => {
// Setting base URL for all API request via axios
axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL;
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
// dev code
} else {
// Prod build code
// Removing console.log from prod
console.log = () => { };
// init analytics here
}
}
export default initializeApp

View File

@ -0,0 +1,14 @@
import { configureStore } from '@reduxjs/toolkit'
import headerSlice from '../Components/features/common/headerSlice'
import modalSlice from '../Components/features/common/modalSlice'
import rightDrawerSlice from '../Components/features/common/rightDrawerSlice'
const combinedReducer = {
header: headerSlice,
rightDrawer: rightDrawerSlice,
modal: modalSlice,
}
export default configureStore({
reducer: combinedReducer
})

View File

@ -1,7 +0,0 @@
export default function ApplicationLogo(props) {
return (
<svg {...props} viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg">
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z" />
</svg>
);
}

View File

@ -1,12 +0,0 @@
export default function Checkbox({ className = '', ...props }) {
return (
<input
{...props}
type="checkbox"
className={
'rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 ' +
className
}
/>
);
}

View File

@ -1,15 +0,0 @@
export default function DangerButton({ className = '', disabled, children, ...props }) {
return (
<button
{...props}
className={
`inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition ease-in-out duration-150 ${
disabled && 'opacity-25'
} ` + className
}
disabled={disabled}
>
{children}
</button>
);
}

View File

@ -0,0 +1,82 @@
import { themeChange } from 'theme-change'
import React, { useEffect, useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import BellIcon from '@heroicons/react/24/outline/BellIcon'
import Bars3Icon from '@heroicons/react/24/outline/Bars3Icon'
import MoonIcon from '@heroicons/react/24/outline/MoonIcon'
import SunIcon from '@heroicons/react/24/outline/SunIcon'
import { openRightDrawer } from './features/common/rightDrawerSlice'
import { RIGHT_DRAWER_TYPES } from '../../../public/utils/globalConstantUtil'
import { Link } from '@inertiajs/react'
import { useForm, usePage } from '@inertiajs/react'
function Header() {
const dispatch = useDispatch()
const { noOfNotifications, pageTitle } = useSelector(state => state.header)
const [theme, setTheme] = useState(localStorage.getItem("theme") ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? "dark" : "light")
);
const { auth } = usePage().props
useEffect(() => {
themeChange(false);
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("theme", theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === "dark" ? "light" : "dark");
}
// const openNotification = () => {
// dispatch(openRightDrawer({ header: "Notifications", bodyType: RIGHT_DRAWER_TYPES.NOTIFICATION }))
// }
const { post } = useForm()
const logoutUser = (e) => {
e.preventDefault()
post('/logout')
}
return (
<div className="navbar sticky top-0 bg-base-100 z-10 shadow-md">
<div className="flex-1">
<label htmlFor="left-sidebar-drawer" className="btn btn-primary drawer-button lg:hidden">
<Bars3Icon className="h-5 inline-block w-5" />
</label>
<h1 className="text-2xl font-semibold ml-2">{pageTitle}</h1>
</div>
<div className="flex-none">
<button onClick={toggleTheme} className="btn btn-ghost">
{theme === "dark" ? <SunIcon className="w-6 h-6" /> : <MoonIcon className="w-6 h-6" />}
</button>
{/* <button className="btn btn-ghost ml-4 btn-circle" onClick={openNotification}>
<div className="indicator">
<BellIcon className="h-6 w-6" />
{noOfNotifications > 0 && <span className="indicator-item badge badge-secondary badge-sm">{noOfNotifications}</span>}
</div>
</button> */}
<div className="dropdown dropdown-end ml-4">
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
<div className="w-10 rounded-full">
<img src={auth.user.foto ? `${auth.user.foto}` : `/fotoSantri/no-pic.png`} alt="profile" />
</div>
</label>
<ul tabIndex={0} className="menu menu-compact dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-52">
<li className="ml-3" >Welcome, {auth.user.nama}</li>
<div className="divider mt-0 mb-0"></div>
<li><Link href={route('profile.edit')}>Profile Settings</Link></li>
<li><a href="#" onClick={logoutUser}>Logout</a></li>
</ul>
</div>
</div>
</div>
)
}
export default Header;

View File

@ -0,0 +1,24 @@
import { useState } from "react"
function InputText({labelTitle, labelStyle, type, containerStyle, defaultValue, placeholder, updateFormValue, updateType}){
const [value, setValue] = useState(defaultValue)
const updateInputValue = (val) => {
setValue(val)
updateFormValue({updateType, value : val})
}
return(
<div className={`form-control w-full ${containerStyle}`}>
<label className="label">
<span className={"label-text text-base-content " + labelStyle}>{labelTitle}</span>
</label>
<input type={type || "text"} value={value} placeholder={placeholder || ""} onChange={(e) => updateInputValue(e.target.value)}className="input input-bordered w-full " />
</div>
)
}
export default InputText

View File

@ -0,0 +1,22 @@
import React, { useEffect } from 'react'
function SearchBar({searchText, styleClass, placeholderText, setSearchText}) {
const updateSearchInput = (value) => {
setSearchText(value)
}
return (
<div className={"inline-block " + styleClass}>
<div className="input-group relative flex flex-wrap items-stretch w-full ">
<input type="search" value={searchText} placeholder={placeholderText || "Search"} onChange={(e) => updateSearchInput(e.target.value)} className="input input-sm input-bordered w-full max-w-xs" />
</div>
</div>
)
}
export default SearchBar

View File

@ -0,0 +1,41 @@
import axios from 'axios'
import capitalize from 'capitalize-the-first-letter'
import React, { useState, useEffect } from 'react'
import InformationCircleIcon from '@heroicons/react/24/outline/InformationCircleIcon'
function SelectBox(props){
const {labelTitle, labelDescription, defaultValue, containerStyle, placeholder, labelStyle, options, updateType, updateFormValue} = props
const [value, setValue] = useState(defaultValue || "")
const updateValue = (newValue) =>{
updateFormValue({updateType, value : newValue})
setValue(newValue)
}
return (
<div className={`inline-block ${containerStyle}`}>
<label className={`label ${labelStyle}`}>
<div className="label-text">{labelTitle}
{labelDescription && <div className="tooltip tooltip-right" data-tip={labelDescription}><InformationCircleIcon className='w-4 h-4'/></div>}
</div>
</label>
<select className="select select-bordered w-full" value={value} onChange={(e) => updateValue(e.target.value)}>
<option disabled value="PLACEHOLDER">{placeholder}</option>
{
options.map((o, k) => {
return <option value={o.value || o.name} key={k}>{o.name}</option>
})
}
</select>
</div>
)
}
export default SelectBox

View File

@ -0,0 +1,24 @@
import { useState } from "react"
function TextAreaInput({labelTitle, labelStyle, type, containerStyle, defaultValue, placeholder, updateFormValue, updateType}){
const [value, setValue] = useState(defaultValue)
const updateInputValue = (val) => {
setValue(val)
updateFormValue({updateType, value : val})
}
return(
<div className={`form-control w-full ${containerStyle}`}>
<label className="label">
<span className={"label-text text-base-content " + labelStyle}>{labelTitle}</span>
</label>
<textarea value={value} className="textarea textarea-bordered w-full" placeholder={placeholder || ""} onChange={(e) => updateInputValue(e.target.value)}></textarea>
</div>
)
}
export default TextAreaInput

View File

@ -0,0 +1,24 @@
import { useState } from "react"
function ToogleInput({labelTitle, labelStyle, type, containerStyle, defaultValue, placeholder, updateFormValue, updateType}){
const [value, setValue] = useState(defaultValue)
const updateToogleValue = () => {
setValue(!value)
updateFormValue({updateType, value : !value})
}
return(
<div className={`form-control w-full ${containerStyle}`}>
<label className="label cursor-pointer">
<span className={"label-text text-base-content " + labelStyle}>{labelTitle}</span>
<input type="checkbox" className="toggle" checked={value} onChange={(e) => updateToogleValue()}/>
</label>
</div>
)
}
export default ToogleInput

View File

@ -1,7 +0,0 @@
export default function InputError({ message, className = '', ...props }) {
return message ? (
<p {...props} className={'text-sm text-red-600 ' + className}>
{message}
</p>
) : null;
}

View File

@ -1,7 +0,0 @@
export default function InputLabel({ value, className = '', children, ...props }) {
return (
<label {...props} className={`block font-medium text-sm text-gray-700 ` + className}>
{value ? value : children}
</label>
);
}

View File

@ -0,0 +1,33 @@
import PageContent from "./PageContent";
import LeftSidebar from "./LeftSidebar";
import { useSelector, useDispatch } from "react-redux";
import { useEffect, useState } from "react";
import { removeNotificationMessage } from "@/Components/features/common/headerSlice";
import { themeChange } from "theme-change";
import { usePage } from "@inertiajs/react";
function Layout({ children }) {
const dispatch = useDispatch();
const { url, } = usePage();
const [showSidebar, setShowSidebar] = useState(true);
useEffect(() => {
// console.log("Route berubah (Inertia):", url);
setShowSidebar(!url.startsWith("/login"));
}, [url]);
useEffect(() => {
themeChange(false);
}, []);
return (
<div className="drawer lg:drawer-open">
<input id="left-sidebar-drawer" type="checkbox" className="drawer-toggle" />
{showSidebar && <LeftSidebar />}
<PageContent>{children}</PageContent>
</div>
);
}
export default Layout;

View File

@ -0,0 +1,52 @@
import { Link, usePage } from '@inertiajs/react';
import SidebarSubmenu from './SidebarSubmenu';
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
import routes from '../Routes/sidebar';
function LeftSidebar() {
const { url } = usePage();
const close = () => {
document.getElementById('left-sidebar-drawer').click();
};
return (
<div className="drawer-side z-30">
<label htmlFor="left-sidebar-drawer" className="drawer-overlay"></label>
<ul className="menu pt-2 w-64 bg-base-100 min-h-full text-base-content">
<button
className="btn btn-ghost bg-base-300 btn-circle z-50 top-0 right-0 mt-4 mr-2 absolute lg:hidden"
onClick={close}
>
<XMarkIcon className="h-5 w-5" />
</button>
<li className="mb-2 font-semibold">
<Link href="/dashboard" className='flex justify-center'><img src="/assets/gogoSantri.png" alt="pp" width={100} /></Link>
</li>
{routes.map((route, k) => (
<li key={k}>
{route.submenu ? (
<SidebarSubmenu {...route} />
) : (
<Link
href={route.path}
className={`relative font-normal ${route.path === url ? 'font-semibold bg-base-200' : ''
}`}
>
{route.path === url && (
<span
className="absolute inset-y-0 left-0 w-1 rounded-tr-md rounded-br-md bg-primary"
aria-hidden="true"
></span>
)}
{route.icon} {route.name}
</Link>
)}
</li>
))}
</ul>
</div>
);
}
export default LeftSidebar;

View File

@ -1,6 +1,12 @@
import { Inertia } from "@inertiajs/inertia";
import React, { useState, useEffect } from "react";
const getImageUrl = (foto) => {
if (!foto) return null;
if (typeof foto === 'string') return `${foto}`;
return URL.createObjectURL(foto);
};
const ModalInput = ({ fields, tableName, options, initialData, onClose, showPayments = false }) => {
const [formData, setFormData] = useState({});
const [errors, setErrors] = useState({});
@ -8,6 +14,7 @@ const ModalInput = ({ fields, tableName, options, initialData, onClose, showPaym
const [paymentDetails, setPaymentDetails] = useState({});
useEffect(() => {
// console.log(initialData)
setFormData(initialData || {});
setSelectedPayments([]);
setPaymentDetails({});
@ -21,17 +28,20 @@ const ModalInput = ({ fields, tableName, options, initialData, onClose, showPaym
}
};
// console.log(initialData)
const handlePaymentChange = (e) => {
const value = e.target.value;
if (!selectedPayments.includes(value)) {
setSelectedPayments([...selectedPayments, value]);
const typeId = e.target.value;
console.log(typeId)
const nominal = options.payment_nominal?.[value] || 0;
const penalty = options.payment_penalty?.[value] || 0;
if (!selectedPayments.includes(typeId)) {
setSelectedPayments([...selectedPayments, typeId]);
const nominal = parseInt(options.payment_nominal[typeId]) || 0;
const penalty = parseInt(options.payment_penalty[typeId]) || 0;
setPaymentDetails({
...paymentDetails,
[value]: {
[typeId]: {
range: 1,
nominal,
penalty,
@ -59,7 +69,6 @@ const ModalInput = ({ fields, tableName, options, initialData, onClose, showPaym
});
};
const handleRemovePayment = (paymentType) => {
setSelectedPayments(selectedPayments.filter((p) => p !== paymentType));
const newDetails = { ...paymentDetails };
@ -69,158 +78,241 @@ const ModalInput = ({ fields, tableName, options, initialData, onClose, showPaym
const handleSubmit = (e) => {
e.preventDefault();
const formDataObj = new FormData();
Object.entries(formData).forEach(([key, value]) => {
if (key === 'foto' && !(value instanceof File)) return;
formDataObj.append(key, value);
const formDataObj = new FormData();
Object.keys(formData).forEach((key) => {
if (key === 'foto' && !(formData[key] instanceof File)) {
return;
}
formDataObj.append(key, formData[key]);
});
formDataObj.append("payments", JSON.stringify(paymentDetails));
if (showPayments) {
const detailsArray = Object.entries(paymentDetails).map(([type_id, detail]) => ({
...detail,
type_id: parseInt(type_id)
}));
// Append items satu per satu
detailsArray.forEach((item, index) => {
formDataObj.append(`items[${index}][type_id]`, item.type_id);
formDataObj.append(`items[${index}][range]`, item.range);
// Kalau mau append nominal & penalty juga bisa, tapi biasanya backend gak butuh karena bisa hitung ulang
});
}
const url = initialData
? `/update${tableName}/${initialData.id}`
: `/add${tableName}`;
const url = initialData ? `/update${tableName}/${initialData.id}` : `/add${tableName}`;
Inertia.post(url, formDataObj, {
forceFormData: true,
onError: (errors) => setErrors(errors),
onSuccess: () => {
document.getElementById("modal_input").checked = false;
document.getElementById('modal_input').checked = false;
setFormData({});
setErrors({});
if (onClose) onClose({});
onClose({});
},
});
};
useEffect(() => {
if (formData.foto instanceof File) {
const url = URL.createObjectURL(formData.foto);
return () => URL.revokeObjectURL(url);
}
}, [formData.foto]);
return (
<div>
<input type="checkbox" id="modal_input" className="modal-toggle" />
<div className="modal" role="dialog">
<div className="modal-box">
<h2 className="font-bold text-lg text-center mb-5">
{initialData ? "Edit Data" : "Tambah Data"}
<div className="modal-box max-w-lg">
<h2 className="font-bold text-xl text-center mb-6 border-b pb-2">
Form Data
</h2>
<form onSubmit={handleSubmit} encType="multipart/form-data">
{Object.keys(fields).map((field) => (
<div key={field} className="mb-2">
<label className="input input-bordered input-secondary flex items-center gap-2">
{field.replace("_", " ")}
{fields[field] === "select" ? (
<form onSubmit={handleSubmit} encType="multipart/form-data" className="space-y-4">
{Object.entries(fields).map(([field, config]) => {
const type = typeof config === "string" ? config : config.type;
const readOnly = typeof config === "object" && config.readonly;
return (
<div key={field} className="form-control">
<label className="label font-semibold capitalize">{field.replace("_", " ")}</label>
{type === "select" ? (
<select
name={field}
value={formData[field] || ""}
onChange={handleChange}
className="select select-bordered w-full select-secondary"
className="select select-info focus:outline-none focus:ring-1 ring-info w-full"
>
<option disabled value="">
Pilih {field.replace("_", " ")}
</option>
{options[field] && Object.entries(options[field]).map(([key, value]) => (
<option key={key} value={key}>{value}</option>
))}
{options[field] &&
Object.entries(options[field]).map(([key, value]) => (
<option key={key} value={key}>
{value}
</option>
))}
</select>
) : fields[field] === "file" ? (
) : type === "file" ? (
<div>
<input
type="file"
name={field}
onChange={handleChange}
className="file-input file-input-info focus:outline-none focus:ring-1 ring-info w-full"
/>
{field === 'foto' && (
<div className="mt-2">
{formData.foto && (
<img
src={getImageUrl(formData.foto)}
alt="Preview Foto"
className="w-32 h-32 object-cover rounded-lg border"
/>
)}
</div>
)}
</div>
) : type === "password" ? (
<input
type="file"
type="password"
name={field}
onChange={handleChange}
className="file-input file-input-bordered w-full file-input-secondary"
value={formData[field] || ""}
className="input input-info focus:outline-none focus:ring-1 ring-info w-full"
placeholder={
initialData
? 'Kosongkan jika tidak ingin mengubah password'
: 'Masukkan password broh'
}
/>
) : (
<input
type={fields[field]}
type={type}
name={field}
value={formData[field] || ""}
onChange={handleChange}
className="grow border-none focus:ring-0"
readOnly={readOnly}
className="input input-info focus:outline-none focus:ring-1 ring-info w-full"
placeholder={`Masukkan ${field.replace("_", " ")}`}
/>
)}
</label>
{errors[field] && <p className="text-red-500 text-sm">{errors[field]}</p>}
</div>
))}
{errors[field] && (
<p className="text-red-500 text-sm mt-1">{errors[field]}</p>
)}
</div>
);
})}
{showPayments && (
<div>
<div className="mb-2">
<label className="input input-bordered input-secondary flex items-center gap-2">
Payment Type
<select onChange={handlePaymentChange} className="select select-bordered w-full select-secondary">
<option disabled value="">Pilih Payment Type</option>
{options.payment_type &&
Object.entries(options.payment_type).map(([key, value]) => (
<option key={key} value={key}>{value}</option>
))}
</select>
</label>
<div className="space-y-4 mt-4">
<div className="form-control">
<label className="label font-semibold">Payment Type</label>
<select
onChange={handlePaymentChange}
className="select select-info focus:outline-none focus:ring-1 ring-info w-full"
>
<option disabled value="">
Pilih Payment Type
</option>
{options.payment_type &&
Object.entries(options.payment_type).map(([id, name]) => (
<option key={id} value={id}>
{name}
</option>
))}
</select>
</div>
{selectedPayments.map((paymentType) => (
<div key={paymentType} className="mb-2 border p-2 rounded relative">
<div
key={paymentType}
className="border rounded-lg p-4 relative bg-gray-50 shadow-sm"
>
<button
type="button"
onClick={() => handleRemovePayment(paymentType)}
className="absolute top-0 right-0 bg-red-500 text-white px-2 py-1 rounded-full text-xs"
className="absolute top-2 right-2 bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded-full text-xs"
>
X
</button>
<div className="mb-2">
<label className="input input-bordered input-secondary flex items-center gap-2">
<div className="form-control mb-2">
<label className="label font-semibold">
{options.payment_type[paymentType]} - Range Bulan
<input
type="number"
min="1"
value={paymentDetails[paymentType].range}
onChange={(e) => handleRangeChange(paymentType, parseInt(e.target.value))}
className="grow border-none focus:ring-0"
/>
</label>
<input
type="number"
min="1"
value={paymentDetails[paymentType].range}
onChange={(e) =>
handleRangeChange(paymentType, parseInt(e.target.value))
}
className="input input-info w-full"
name="range"
/>
</div>
<div className="mb-2">
<label className="input input-bordered input-secondary flex items-center gap-2">
Nominal
<input type="text" value={paymentDetails[paymentType].nominal} readOnly className="grow border-none focus:ring-0 bg-gray-100" />
</label>
<div className="form-control mb-2">
<label className="label font-semibold">Nominal</label>
<input
type="text"
value={paymentDetails[paymentType].nominal}
readOnly
className="input bg-gray-100 w-full"
/>
</div>
<div className="mb-2">
<label className="input input-bordered input-secondary flex items-center gap-2">
Penalty
<input type="text" value={paymentDetails[paymentType].penalty} readOnly className="grow border-none focus:ring-0 bg-gray-100" />
</label>
<div className="form-control mb-2">
<label className="label font-semibold">Penalty</label>
<input
type="text"
value={paymentDetails[paymentType].penalty}
readOnly
className="input bg-gray-100 w-full"
/>
</div>
</div>
))}
<div className="mb-2">
<label className="input input-bordered input-secondary flex items-center gap-2">
Total Amount
<input
type="text"
value={Object.values(paymentDetails).reduce((sum, p) => sum + (p.amount || 0), 0)}
readOnly
className="grow border-none focus:ring-0 bg-gray-100"
/>
</label>
<div className="form-control mt-4">
<label className="label font-semibold">Total Amount</label>
<input
type="text"
value={Object.values(paymentDetails).reduce(
(sum, p) => sum + (p.amount || 0),
0
)}
readOnly
className="input bg-gray-100 w-full"
/>
</div>
</div>
)}
<button type="submit" className="btn btn-secondary w-full mt-3">
{initialData ? "Simpan Perubahan" : "Tambah Data"}
<button type="submit" className="btn btn-info text-white font-bold w-full mt-4">
{initialData ? "submit" : "Tambah Data"}
</button>
</form>
</div>
<label className="modal-backdrop" htmlFor="modal_input">Close</label>
<label className="modal-backdrop" htmlFor="modal_input">
Close
</label>
</div>
</div>
);
};
export default ModalInput;
export default ModalInput

View File

@ -0,0 +1,57 @@
import Header from "./Header";
import { Suspense, useEffect, useRef, useState } from "react";
import SuspenseContent from "./SuspenseContent";
import { useSelector } from 'react-redux';
import { usePage } from "@inertiajs/react";
import Swal from "sweetalert2"
function PageContent({ children }) {
const mainContentRef = useRef(null);
const { pageTitle } = useSelector(state => state.header);
const { url } = usePage(); // Ambil URL dari Inertia
const [isLoginPage, setIsLoginPage] = useState(false);
const { flash } = usePage().props;
useEffect(() => {
if (flash.success) {
Swal.fire({
icon: 'success',
title: 'Success',
text: flash.success
});
} else if (flash.error) {
Swal.fire({
icon: 'success',
title: 'Success',
text: flash.success
});
}
}, [flash]);
useEffect(() => {
mainContentRef.current?.scroll({
top: 0,
behavior: "smooth"
});
}, [pageTitle]);
// Update state saat route berubah
useEffect(() => {
setIsLoginPage(url === "/login");
}, [url]);
return (
<div className="drawer-content flex flex-col">
{!isLoginPage && <Header />}
<main className="flex-1 overflow-y-auto md:pt-4 pt-4 px-6 bg-base-200" ref={mainContentRef}>
<Suspense fallback={<SuspenseContent />}>
<div className="min-h-screen">
{children}
</div>
</Suspense>
</main>
</div>
);
}
export default PageContent;

View File

@ -1,15 +0,0 @@
export default function PrimaryButton({ className = '', disabled, children, ...props }) {
return (
<button
{...props}
className={
`inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150 ${
disabled && 'opacity-25'
} ` + className
}
disabled={disabled}
>
{children}
</button>
);
}

View File

@ -1,16 +0,0 @@
export default function SecondaryButton({ type = 'button', className = '', disabled, children, ...props }) {
return (
<button
{...props}
type={type}
className={
`inline-flex items-center px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 transition ease-in-out duration-150 ${
disabled && 'opacity-25'
} ` + className
}
disabled={disabled}
>
{children}
</button>
);
}

View File

@ -0,0 +1,45 @@
import ChevronDownIcon from '@heroicons/react/24/outline/ChevronDownIcon';
import { useEffect, useState } from 'react';
import { Link, usePage } from '@inertiajs/react';
function SidebarSubmenu({ submenu, name, icon }) {
const { url } = usePage();
const [isExpanded, setIsExpanded] = useState(false);
useEffect(() => {
if (submenu.some(m => m.path === url)) {
setIsExpanded(true);
}
}, [url, submenu]);
return (
<div className="flex flex-col">
<div className="w-full block" onClick={() => setIsExpanded(!isExpanded)}>
{icon} {name}
<ChevronDownIcon
className={`w-5 h-5 mt-1 float-right delay-400 duration-500 transition-all ${isExpanded ? 'rotate-180' : ''
}`}
/>
</div>
<div className={`w-full ${isExpanded ? '' : 'hidden'}`}>
<ul className="menu menu-compact">
{submenu.map((m, k) => (
<li key={k}>
<Link href={m.path}>
{m.icon} {m.name}
{url === m.path && (
<span
className="absolute mt-1 mb-1 inset-y-0 left-0 w-1 rounded-tr-md rounded-br-md bg-primary"
aria-hidden="true"
></span>
)}
</Link>
</li>
))}
</ul>
</div>
</div>
);
}
export default SidebarSubmenu;

View File

@ -0,0 +1,9 @@
function SuspenseContent() {
return (
<div className="w-full h-screen text-gray-300 dark:text-gray-200 bg-base-100">
Loading...
</div>
)
}
export default SuspenseContent

View File

@ -0,0 +1,40 @@
import { useDispatch, useSelector } from 'react-redux'
import axios from 'axios'
import { CONFIRMATION_MODAL_CLOSE_TYPES } from '../../../../../../public/utils/globalConstantUtil'
import { deleteLead } from '../../leads/leadSlice'
import { showNotification } from '../headerSlice'
function ConfirmationModalBody({ extraObject, closeModal }) {
const dispatch = useDispatch()
const { message, type, _id, index } = extraObject
const proceedWithYes = async () => {
if (type === CONFIRMATION_MODAL_CLOSE_TYPES.LEAD_DELETE) {
// positive response, call api or dispatch redux function
dispatch(deleteLead({ index }))
dispatch(showNotification({ message: "Lead Deleted!", status: 1 }))
}
closeModal()
}
return (
<>
<p className=' text-xl mt-8 text-center'>
{message}
</p>
<div className="modal-action mt-12">
<button className="btn btn-outline " onClick={() => closeModal()}>Cancel</button>
<button className="btn btn-primary w-36" onClick={() => proceedWithYes()}>Yes</button>
</div>
</>
)
}
export default ConfirmationModalBody

View File

@ -0,0 +1,15 @@
function NotificationBodyRightDrawer(){
return(
<>
{
[...Array(15)].map((_, i) => {
return <div key={i} className={"grid mt-3 card bg-base-200 rounded-box p-3" + (i < 5 ? " bg-blue-100" : "")}>
{i % 2 === 0 ? `Your sales has increased by 30% yesterday` : `Total likes for instagram post - New launch this week, has crossed 100k `}
</div>
})
}
</>
)
}
export default NotificationBodyRightDrawer

View File

@ -0,0 +1,30 @@
import { createSlice } from '@reduxjs/toolkit'
export const headerSlice = createSlice({
name: 'header',
initialState: {
pageTitle: "Home",
noOfNotifications: 15,
newNotificationMessage: "",
newNotificationStatus: 1,
},
reducers: {
setPageTitle: (state, action) => {
state.pageTitle = action.payload
},
removeNotificationMessage: (state, action) => {
state.newNotificationMessage = ""
},
showNotification: (state, action) => {
state.newNotificationMessage = action.payload.message
state.newNotificationStatus = action.payload.status
},
}
})
export const { setPageTitle, removeNotificationMessage, showNotification } = headerSlice.actions
export default headerSlice.reducer

View File

@ -0,0 +1,35 @@
import { createSlice } from '@reduxjs/toolkit'
export const modalSlice = createSlice({
name: 'modal',
initialState: {
title: "", // current title state management
isOpen : false, // modal state management for opening closing
bodyType : "", // modal content management
size : "", // modal content management
extraObject : {},
},
reducers: {
openModal: (state, action) => {
const {title, bodyType, extraObject, size} = action.payload
state.isOpen = true
state.bodyType = bodyType
state.title = title
state.size = size || 'md'
state.extraObject = extraObject
},
closeModal: (state, action) => {
state.isOpen = false
state.bodyType = ""
state.title = ""
state.extraObject = {}
},
}
})
export const { openModal, closeModal } = modalSlice.actions
export default modalSlice.reducer

View File

@ -0,0 +1,33 @@
import { createSlice } from '@reduxjs/toolkit'
export const rightDrawerSlice = createSlice({
name: 'rightDrawer',
initialState: {
header: "", // current title state management
isOpen : false, // right drawer state management for opening closing
bodyType : "", // right drawer content management
extraObject : {},
},
reducers: {
openRightDrawer: (state, action) => {
const {header, bodyType, extraObject} = action.payload
state.isOpen = true
state.bodyType = bodyType
state.header = header
state.extraObject = extraObject
},
closeRightDrawer: (state, action) => {
state.isOpen = false
state.bodyType = ""
state.header = ""
state.extraObject = {}
},
}
})
export const { openRightDrawer, closeRightDrawer } = rightDrawerSlice.actions
export default rightDrawerSlice.reducer

Some files were not shown because too many files have changed in this diff Show More