Merge pull request #18 from alealien666/dev

Dev
This commit is contained in:
AleAlien 2025-06-07 18:58:26 +07:00 committed by GitHub
commit d73a8e636c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
143 changed files with 9653 additions and 532 deletions

View File

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

View File

@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use Inertia\Response;
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*/
public function create(): Response
{
return Inertia::render('Auth/Login', [
'canResetPassword' => Route::has('password.request'),
'status' => session('status'),
]);
}
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
session()->flash('success', 'Login berhasil!');
return redirect()->intended(RouteServiceProvider::HOME);
}
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
session()->flash('success', 'Logout berhasil!');
return redirect('/');
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password view.
*/
public function show(): Response
{
return Inertia::render('Auth/ConfirmPassword');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(RouteServiceProvider::HOME);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(RouteServiceProvider::HOME);
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class EmailVerificationPromptController extends Controller
{
/**
* Display the email verification prompt.
*/
public function __invoke(Request $request): RedirectResponse|Response
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(RouteServiceProvider::HOME)
: Inertia::render('Auth/VerifyEmail', ['status' => session('status')]);
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class NewPasswordController extends Controller
{
/**
* Display the password reset view.
*/
public function create(Request $request): Response
{
return Inertia::render('Auth/ResetPassword', [
'nis' => $request->nis,
'token' => $request->route('token'),
]);
}
/**
* Handle an incoming new password request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => 'required',
'nis' => 'required|nis',
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// 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('nis', 'password', 'password_confirmation', 'token'),
function ($user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
if ($status == Password::PASSWORD_RESET) {
return redirect()->route('login')->with('status', __($status));
}
throw ValidationException::withMessages([
'nis' => [trans($status)],
]);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class PasswordController extends Controller
{
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validate([
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back();
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): Response
{
return Inertia::render('Auth/ForgotPassword', [
'status' => session('status'),
]);
}
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => 'required|email',
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
if ($status == Password::RESET_LINK_SENT) {
return back()->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => [trans($status)],
]);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Inertia\Inertia;
use Inertia\Response;
class RegisteredUserController extends Controller
{
/**
* Display the registration view.
*/
public function create(): Response
{
return Inertia::render('Auth/Register');
}
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
// '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,
'nis' => $request->nis,
'password' => Hash::make($request->password),
'level' => 2
]);
event(new Registered($user));
Auth::login($user);
return redirect(RouteServiceProvider::HOME);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
}
}

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,62 +4,161 @@
use App\Models\Payment; use App\Models\Payment;
use Illuminate\Http\Request; use Illuminate\Http\Request;
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 class PaymentController extends Controller
{ {
/** public function indexManualPayment(cekDenda $cekDenda, GenerateMonthlyBill $generateMonthlyBill)
* Display a listing of the resource.
*/
public function index()
{ {
//
$paymentTypes = PaymentType::get(['id', 'payment_type', 'nominal']);
$paymentPenalties = DetailPayment::with('paymentType')
->get()
->pluck('penalty', 'type_id');
$santri = User::with([
'payments.detailPayments.paymentType',
])->where('level', 2)->paginate(10);
return Inertia::render('list-admin/payment/ManualPayment', [
'santri' => $santri,
'penalty' => 0,
'fields' => [
'nis' => ['type' => 'text', 'readonly' => true],
'nama' => ['type' => 'text', 'readonly' => true],
'status_santri' => ['type' => 'text', 'readonly' => true],
],
'options' => [
'payment_type' => $paymentTypes->pluck('payment_type', 'id'),
'payment_nominal' => $paymentTypes->pluck('nominal', 'id'),
'payment_penalty' => $paymentPenalties
]
]);
} }
/** public function manualPayment(Request $request, $userId)
* Show the form for creating a new resource.
*/
public function create()
{ {
// $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();
* Store a newly created resource in storage.
*/ try {
public function store(Request $request) $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([
* Display the specified resource. 'payment_status' => 'success',
*/ 'amount_payment' => 0,
public function show(Payment $payment) '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++) {
* Show the form for editing the specified resource. if ($i > 0 || $lastDetail) {
*/ $bulan++;
public function edit(Payment $payment) if ($bulan > 12) {
{ $bulan = 1;
// $tahun++;
}
} }
/** DetailPayment::create([
* Update the specified resource in storage. 'payment_id' => $newPayment->id,
*/ 'payment_month' => $bulan,
public function update(Request $request, Payment $payment) 'payment_year' => $tahun,
{ 'amount' => $nominal,
// 'penalty' => 0,
'status' => 'paid',
'type_id' => $typeId,
]);
$totalAmount += $nominal;
}
} }
/** $newPayment->update(['amount_payment' => $totalAmount]);
* Remove the specified resource from storage.
*/ DB::commit();
public function destroy(Payment $payment)
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

@ -4,6 +4,7 @@
use App\Models\PaymentType; use App\Models\PaymentType;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Inertia\Inertia;
class PaymentTypeController extends Controller class PaymentTypeController extends Controller
{ {
@ -12,54 +13,64 @@ class PaymentTypeController extends Controller
*/ */
public function index() public function index()
{ {
// $paymentType = PaymentType::all();
return Inertia::render('list-admin/payment/PaymentType', [
'paymentType' => $paymentType,
'fields' => [
'payment_type' => 'text',
'nominal' => 'number',
]
]);
} }
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request) public function store(Request $request)
{ {
// $request->validate([
'payment_type' => 'required|string',
'nominal' => 'required|numeric'
], [
'payment_type.required' => 'wajib mengisi payment type',
'nominal.required' => 'wajib mengisi nominal pembayaran'
]);
try {
PaymentType::create([
'payment_type' => $request->payment_type,
'nominal' => $request->nominal
]);
return redirect()->back()->with('success', 'berhasil insert data tipe pembayaran');
} catch (\Throwable $th) {
return redirect()->back()->with('error', 'Data gagal di tambahkan' . $th->getMessage());
}
} }
/** public function update(Request $request, PaymentType $paymentType, $id)
* Display the specified resource.
*/
public function show(PaymentType $paymentType)
{ {
// $request->validate([
'payment_type' => 'required|string'
], [
'payment_type.required' => 'wajib mengisi payment type'
]);
try {
$paymentType = PaymentType::findOrFail($id);
$paymentType->payment_type = $request->payment_type;
$paymentType->update();
return redirect()->back()->with('success', 'Berhasil Update Data');
} catch (\Throwable $th) {
return redirect()->back()->with('error', 'Gagal Mengupdate Data');
}
} }
/** public function destroy(PaymentType $paymentType, $id)
* Show the form for editing the specified resource.
*/
public function edit(PaymentType $paymentType)
{ {
// $paymentType = PaymentType::findOrFail($id);
}
/** $paymentType->delete();
* Update the specified resource in storage.
*/
public function update(Request $request, PaymentType $paymentType)
{
//
}
/** return redirect()->back()->with('success', 'Berhasil Menghapus Data');
* Remove the specified resource from storage.
*/
public function destroy(PaymentType $paymentType)
{
//
} }
} }

View File

@ -0,0 +1,76 @@
<?php
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
{
public function edit(Request $request): Response
{
// $user = User::all();
return Inertia::render('Profile/Profile');
}
public function updateProfile(Request $request, $id)
{
$request->validate([
'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',
]);
$santri = User::findOrFail($id);
try {
$updateData = [
'nama' => $request->nama,
'alamat' => $request->alamat,
'jk' => $request->jk,
'tanggal_lahir' => $request->tanggal_lahir,
];
if ($request->hasFile('foto')) {
if ($santri->foto && File::exists(public_path($santri->foto))) {
File::delete(public_path($santri->foto));
}
$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);
}
// 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,61 +2,169 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Santri; use App\Models\User;
use App\Models\Wallet;
use Illuminate\Http\Request; 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 class SantriController extends Controller
{ {
/** public function index()
* Display a listing of the resource.
*/
public function index() {}
/**
* Show the form for creating a new resource.
*/
public function create()
{ {
// $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',
'jk' => 'select',
'level' => 'select',
'tanggal_lahir' => 'date',
'foto' => 'file',
],
'options' => [
'status_santri' => ['lulus' => 'Lulus', 'aktif' => 'Aktif'],
'role_santri' => ['santri' => 'Santri', 'pengurus' => 'Pengurus'],
'jk' => ['laki laki' => 'Laki-Laki', 'perempuan' => 'Perempuan'],
'level' => [1 => 'Admin', 2 => 'User']
],
]);
} }
/**
* Store a newly created resource in storage.
*/
public function store(Request $request) public function store(Request $request)
{ {
// $request->validate([
'nis' => 'required',
'password' => 'required',
'level' => 'required',
'nama' => 'required',
'alamat' => 'required',
'status_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',
'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;
if ($request->hasFile('foto')) {
$foto = $request->file('foto');
$fotoName = time() . '_' . $foto->getClientOriginalName();
$foto->move(public_path('fotoSantri'), $fotoName);
$fotoPath = 'fotoSantri/' . $fotoName;
} }
/** try {
* Display the specified resource. $santri = User::create([
*/ 'nis' => $request->nis,
public function show(Santri $santri) 'password' => Hash::make($request->password),
{ 'level' => $request->level,
// 'nama' => $request->nama,
'alamat' => $request->alamat,
'status_santri' => $request->status_santri,
'jk' => $request->jk,
'tanggal_lahir' => $request->tanggal_lahir,
'no_telp' => $request->no_telp,
'foto' => $fotoPath
]);
$santri->wallet()->create(['saldo' => 0]);
// dd($santri);
return redirect()->back()->with('success', 'Data berhasil ditambahkan');
} catch (\Throwable $th) {
// dd($th->getMessage());
return redirect()->back()->with('error', 'Data gagal ditambahkan : ' . $th->getMessage());
}
} }
/** public function update(Request $request, $id)
* Show the form for editing the specified resource.
*/
public function edit(Santri $santri)
{ {
// $request->validate([
'nis' => 'required',
'password' => 'nullable',
'level' => 'required',
'nama' => 'required',
'alamat' => 'required',
'status_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',
'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 = User::findOrFail($id);
$updateData = [
'nis' => $request->nis,
'level' => $request->level,
'nama' => $request->nama,
'alamat' => $request->alamat,
'status_santri' => $request->status_santri,
'jk' => $request->jk,
'tanggal_lahir' => $request->tanggal_lahir,
];
if ($request->hasFile('foto')) {
if ($santri->foto && File::exists(public_path($santri->foto))) {
File::delete(public_path($santri->foto));
} }
/** $foto = $request->file('foto');
* Update the specified resource in storage. $fotoName = time() . '_' . $foto->getClientOriginalName();
*/ $foto->move(public_path('fotoSantri'), $fotoName);
public function update(Request $request, Santri $santri) $updateData['foto'] = 'fotoSantri/' . $fotoName;
{ }
// if ($request->filled('password')) {
$updateData['password'] = Hash::make($request->password);
} }
/** // return $updateData;
* Remove the specified resource from storage.
*/ $updateStatus = $santri->update($updateData);
public function destroy(Santri $santri)
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($id)
{ {
// $santri = User::findOrFail($id);
$santri->delete();
File::delete('fotoSantri/' . basename($santri->foto));
return redirect()->back()->with('success', 'data berhasil di hapus');
} }
} }

View File

@ -2,64 +2,18 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Wallet; use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Inertia\Inertia;
class WalletController extends Controller class WalletController extends Controller
{ {
/** public function walletUser()
* Display a listing of the resource.
*/
public function index()
{ {
// $wallet = User::with('wallet')
} ->where('level', 2)
->paginate(10);
/** return Inertia::render('list-admin/payment/WalletUser', compact('wallet'));
* 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)
{
//
} }
} }

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

@ -36,6 +36,8 @@ class Kernel extends HttpKernel
\Illuminate\View\Middleware\ShareErrorsFromSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class, \App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\HandleInertiaRequests::class,
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
], ],
'api' => [ 'api' => [

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
/**
* The root template that is loaded on the first page visit.
*
* @var string
*/
protected $rootView = 'app';
/**
* Determine the current asset version.
*/
public function version(Request $request): string|null
{
return parent::version($request);
}
/**
* Define the props that are shared by default.
*
* @return array<string, mixed>
*/
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user(),
],
'flash' => [
'success' => fn() => $request->session()->get('success'),
'error' => fn() => $request->session()->get('error'),
],
];
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
public function rules(): array
{
return [
'nis' => ['required', 'string'],
'password' => ['required', 'string'],
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only('nis', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'nis' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'nis' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the rate limiting throttle key for the request.
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->string('nis')) . '|' . $this->ip());
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ProfileUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)],
];
}
}

View File

@ -10,8 +10,17 @@ class DetailPayment extends Model
use HasFactory; use HasFactory;
protected $guarded = ['id']; protected $guarded = ['id'];
protected $table = 'detail_payments';
public function payment() public static function cekTunggakan($santri_id)
{
return self::where('status', 'unpaid')->whereHas('payment', function ($query) use ($santri_id) {
$query->where('santri_id', $santri_id);
})->orderBy('payment_year', 'asc')->orderBy('payment_month')->get();
}
public function payments()
{ {
return $this->belongsTo(Payment::class, 'payment_id', 'id'); return $this->belongsTo(Payment::class, 'payment_id', 'id');
} }

View File

@ -11,9 +11,9 @@ class Payment extends Model
protected $guarded = ['id']; 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() public function wallet()

View File

@ -1,28 +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'];
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> * @var array<int, string>
*/ */
protected $fillable = [ protected $guarded = [
'name', 'id'
'email',
'password',
]; ];
/** /**
@ -38,12 +36,14 @@ class User extends Authenticatable
* *
* @var array<string, string> * @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'); 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() public function walletTransactions()

View File

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

View File

@ -17,7 +17,7 @@ class RouteServiceProvider extends ServiceProvider
* *
* @var string * @var string
*/ */
public const HOME = '/home'; public const HOME = '/dashboard';
/** /**
* Define your route model bindings, pattern filters, and other route configuration. * Define your route model bindings, pattern filters, and other route configuration.

View File

@ -0,0 +1,87 @@
<?php
namespace App\Services;
use App\Models\DetailPayment;
use App\Models\Payment;
use App\Models\PaymentType;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class GenerateMonthlyBill
{
public function generateAutoBill()
{
$currentMonth = Carbon::now()->month;
$currentYear = Carbon::now()->year;
$previousMonth = Carbon::now()->subMonth()->month;
$previousYear = Carbon::now()->subMonth()->year;
DB::beginTransaction();
try {
$santriPayments = Payment::with('detailPayments')->get();
foreach ($santriPayments as $prevPay) {
$existingPayment = Payment::where('santri_id', $prevPay->santri_id)
->whereMonth('created_at', $currentMonth)
->whereYear('created_at', $currentYear)
->first();
if (!$existingPayment) {
$newPayment = Payment::create([
'payment_status' => 'pending',
'amount_payment' => 0,
'santri_id' => $prevPay->santri_id,
'wallet_id' => $prevPay->wallet_id,
'bank' => null,
'no_va' => null,
'expired_at' => null,
'payment_method' => null,
]);
} else {
$newPayment = $existingPayment;
}
$previousPayment = Payment::where('santri_id', $prevPay->santri_id)
->whereMonth('created_at', $previousMonth)
->whereYear('created_at', $previousYear)
->first();
if (!$previousPayment) {
continue;
}
$previousDetails = $previousPayment->detailPayments;
foreach ($previousDetails as $previousDetail) {
$existingBill = DetailPayment::where('payment_id', $newPayment->id)
->where('type_id', $previousDetail->type_id)
->where('payment_month', $currentMonth)
->where('payment_year', $currentYear)
->exists();
if (!$existingBill) {
$paymentType = PaymentType::find($previousDetail->type_id);
$amount = $paymentType ? $paymentType->amount : 0;
DetailPayment::create([
'payment_id' => $newPayment->id,
'type_id' => $previousDetail->type_id,
'amount' => $amount,
'status' => 'unpaid',
'payment_month' => $currentMonth,
'payment_year' => $currentYear,
'penalty' => null,
]);
}
}
}
DB::commit();
return ['message' => 'Berhasil menambah data'];
} catch (\Exception $e) {
DB::rollBack();
return ['error' => 'Gagal menambah data: ' . $e->getMessage()];
}
}
}

44
app/Services/cekDenda.php Normal file
View File

@ -0,0 +1,44 @@
<?php
namespace App\Services;
use App\Models\DetailPayment;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class cekDenda
{
public function applyPenalty()
{
$previousMonth = Carbon::now()->subMonth()->month;
$previousYear = Carbon::now()->subMonth()->year;
$currentDay = Carbon::now()->day;
$penaltyAmount = 20000;
if ($currentDay >= 10) {
DB::beginTransaction();
try {
$unpaidPayments = DetailPayment::where('status', 'unpaid')
->where('payment_month', $previousMonth)
->where('payment_year', $previousYear)
->get();
foreach ($unpaidPayments as $payment) {
if (is_null($payment->penalty)) {
$payment->update([
'penalty' => $penaltyAmount
]);
}
}
DB::commit();
return ['message' => 'Penalty applied successfully'];
} catch (\Exception $e) {
DB::rollBack();
return ['error' => $e->getMessage()];
}
}
return ['message' => 'Not yet time for penalty application'];
}
}

View File

@ -7,12 +7,15 @@
"require": { "require": {
"php": "^8.1", "php": "^8.1",
"guzzlehttp/guzzle": "^7.2", "guzzlehttp/guzzle": "^7.2",
"inertiajs/inertia-laravel": "^0.6.3",
"laravel/framework": "^10.0", "laravel/framework": "^10.0",
"laravel/sanctum": "^3.2", "laravel/sanctum": "^3.2",
"laravel/tinker": "^2.8" "laravel/tinker": "^2.8",
"tightenco/ziggy": "^2.0"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",
"laravel/breeze": "^1.29",
"laravel/pint": "^1.0", "laravel/pint": "^1.0",
"laravel/sail": "^1.18", "laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.4.4",

206
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "bfe12996eeecb6fdc8713a9fd9d431f8", "content-hash": "44d8185187ad62ec42dc2fe932715ec5",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@ -1054,6 +1054,78 @@
], ],
"time": "2023-12-03T19:50:20+00:00" "time": "2023-12-03T19:50:20+00:00"
}, },
{
"name": "inertiajs/inertia-laravel",
"version": "v0.6.11",
"source": {
"type": "git",
"url": "https://github.com/inertiajs/inertia-laravel.git",
"reference": "2a1e19048f95c0e4adb2b2733f9119e49c4fc09f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/2a1e19048f95c0e4adb2b2733f9119e49c4fc09f",
"reference": "2a1e19048f95c0e4adb2b2733f9119e49c4fc09f",
"shasum": ""
},
"require": {
"ext-json": "*",
"laravel/framework": "^6.0|^7.0|^8.74|^9.0|^10.0",
"php": "^7.2|~8.0.0|~8.1.0|~8.2.0|~8.3.0"
},
"require-dev": {
"mockery/mockery": "^1.3.3",
"orchestra/testbench": "^4.0|^5.0|^6.4|^7.0|^8.0",
"phpunit/phpunit": "^8.0|^9.5.8",
"roave/security-advisories": "dev-master"
},
"suggest": {
"ext-pcntl": "Recommended when running the Inertia SSR server via the `inertia:start-ssr` artisan command."
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Inertia\\ServiceProvider"
]
}
},
"autoload": {
"files": [
"./helpers.php"
],
"psr-4": {
"Inertia\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jonathan Reinink",
"email": "jonathan@reinink.ca",
"homepage": "https://reinink.ca"
}
],
"description": "The Laravel adapter for Inertia.js.",
"keywords": [
"inertia",
"laravel"
],
"support": {
"issues": "https://github.com/inertiajs/inertia-laravel/issues",
"source": "https://github.com/inertiajs/inertia-laravel/tree/v0.6.11"
},
"funding": [
{
"url": "https://github.com/reinink",
"type": "github"
}
],
"time": "2023-10-27T10:59:02+00:00"
},
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v10.48.25", "version": "v10.48.25",
@ -5340,6 +5412,76 @@
], ],
"time": "2024-11-08T15:28:48+00:00" "time": "2024-11-08T15:28:48+00:00"
}, },
{
"name": "tightenco/ziggy",
"version": "v2.5.0",
"source": {
"type": "git",
"url": "https://github.com/tighten/ziggy.git",
"reference": "2b574ba281546884b7bdde6eefa451ba7e0b52f7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tighten/ziggy/zipball/2b574ba281546884b7bdde6eefa451ba7e0b52f7",
"reference": "2b574ba281546884b7bdde6eefa451ba7e0b52f7",
"shasum": ""
},
"require": {
"ext-json": "*",
"laravel/framework": ">=9.0",
"php": ">=8.1"
},
"require-dev": {
"laravel/folio": "^1.1",
"orchestra/testbench": "^7.0 || ^8.0 || ^9.0",
"pestphp/pest": "^2.26",
"pestphp/pest-plugin-laravel": "^2.4"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Tighten\\Ziggy\\ZiggyServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Tighten\\Ziggy\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Daniel Coulbourne",
"email": "daniel@tighten.co"
},
{
"name": "Jake Bathman",
"email": "jake@tighten.co"
},
{
"name": "Jacob Baker-Kretzmar",
"email": "jacob@tighten.co"
}
],
"description": "Use your Laravel named routes in JavaScript.",
"homepage": "https://github.com/tighten/ziggy",
"keywords": [
"Ziggy",
"javascript",
"laravel",
"routes"
],
"support": {
"issues": "https://github.com/tighten/ziggy/issues",
"source": "https://github.com/tighten/ziggy/tree/v2.5.0"
},
"time": "2025-01-23T00:32:18+00:00"
},
{ {
"name": "tijsverkoyen/css-to-inline-styles", "name": "tijsverkoyen/css-to-inline-styles",
"version": "v2.3.0", "version": "v2.3.0",
@ -5798,6 +5940,68 @@
}, },
"time": "2020-07-09T08:09:16+00:00" "time": "2020-07-09T08:09:16+00:00"
}, },
{
"name": "laravel/breeze",
"version": "v1.29.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/breeze.git",
"reference": "22c53b84b7fff91b01a318d71a10dfc251e92849"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/breeze/zipball/22c53b84b7fff91b01a318d71a10dfc251e92849",
"reference": "22c53b84b7fff91b01a318d71a10dfc251e92849",
"shasum": ""
},
"require": {
"illuminate/console": "^10.17",
"illuminate/filesystem": "^10.17",
"illuminate/support": "^10.17",
"illuminate/validation": "^10.17",
"php": "^8.1.0"
},
"require-dev": {
"orchestra/testbench": "^8.0",
"phpstan/phpstan": "^1.10"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Breeze\\BreezeServiceProvider"
]
},
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Laravel\\Breeze\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Minimal Laravel authentication scaffolding with Blade and Tailwind.",
"keywords": [
"auth",
"laravel"
],
"support": {
"issues": "https://github.com/laravel/breeze/issues",
"source": "https://github.com/laravel/breeze"
},
"time": "2024-03-04T14:35:21+00:00"
},
{ {
"name": "laravel/pint", "name": "laravel/pint",
"version": "v1.20.0", "version": "v1.20.0",

View File

@ -1,35 +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('nis');
$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');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('santris');
}
};

View File

@ -15,8 +15,14 @@ public function up(): void
$table->id(); $table->id();
$table->string('nis'); $table->string('nis');
$table->string('password'); $table->string('password');
$table->string('role'); $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->rememberToken();
$table->timestamps(); $table->timestamps();
}); });

View File

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

View File

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

View File

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

View File

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

View File

@ -14,10 +14,11 @@ public function up(): void
Schema::create('detail_payments', function (Blueprint $table) { Schema::create('detail_payments', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('payment_id')->constrained('payments')->onDelete('cascade'); $table->foreignId('payment_id')->constrained('payments')->onDelete('cascade');
$table->enum('mounth', ['januari', 'februari', 'maret', 'april', 'mei', 'juni', 'juli', 'agustus', 'september', 'oktober', 'november', 'desember']); $table->enum('status', ['paid', 'unpaid']);
$table->enum('status', ['bayar', 'belum bayar']); $table->decimal('amount')->nullable();
$table->float('amount'); $table->decimal('penalty')->nullable();
$table->float('penalty')->nullable(); $table->integer('payment_month');
$table->integer('payment_year');
$table->foreignId('type_id')->constrained('payment_types')->onDelete('cascade'); $table->foreignId('type_id')->constrained('payment_types')->onDelete('cascade');
$table->timestamps(); $table->timestamps();
}); });

View File

@ -4,6 +4,8 @@
// use Illuminate\Database\Console\Seeds\WithoutModelEvents; // use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
class DatabaseSeeder extends Seeder class DatabaseSeeder extends Seeder
{ {
@ -12,11 +14,31 @@ class DatabaseSeeder extends Seeder
*/ */
public function run(): void public function run(): void
{ {
// \App\Models\User::factory(10)->create(); User::insert([
[
// \App\Models\User::factory()->create([ 'nis' => '1234567890',
// 'name' => 'Test User', 'password' => Hash::make('password123'),
// 'email' => 'test@example.com', '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'
]
],);
} }
} }

10
jsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["resources/js/*"],
"ziggy-js": ["./vendor/tightenco/ziggy"]
}
},
"exclude": ["node_modules", "public"]
}

3197
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,8 +5,28 @@
"build": "vite build" "build": "vite build"
}, },
"devDependencies": { "devDependencies": {
"@headlessui/react": "^1.4.2",
"@tailwindcss/forms": "^0.5.3",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.12",
"axios": "^1.1.2", "axios": "^1.1.2",
"daisyui": "^4.12.23",
"laravel-vite-plugin": "^0.7.2", "laravel-vite-plugin": "^0.7.2",
"postcss": "^8.4.31",
"react": "^18.2.0",
"tailwindcss": "^3.2.1",
"vite": "^4.0.0" "vite": "^4.0.0"
},
"dependencies": {
"@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"
} }
} }

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

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.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

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",
});

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

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

@ -0,0 +1,39 @@
import { Inertia } from "@inertiajs/inertia";
import React, { useState } from "react";
const DeleteButton = ({ isOpen, onClose, item, tableName }) => {
if (!isOpen || !item) return null
const handleDelete = () => {
Inertia.post(`/delete${tableName}/${item.id}`, {
onSuccess: () => {
onClose();
},
})
}
return (
<div className="modal modal-open">
<div className="modal-box">
<h2 className="font-bold text-lg text-center mb-5">
Konfirmasi Hapus Data
</h2>
<p className="text-center mb-4">
Apakah Anda yakin ingin menghapus <strong>{item.nama}</strong>?
</p>
<div className="flex justify-center gap-4">
<button onClick={handleDelete} className="btn btn-error text-white">
Hapus
</button>
<button onClick={onClose} className="btn btn-secondary text-white">
Batal
</button>
</div>
</div>
<label className="modal-backdrop" onClick={onClose}></label>
</div>
)
}
export default DeleteButton

View File

@ -0,0 +1,91 @@
import { useState, createContext, useContext, Fragment } from 'react';
import { Link } from '@inertiajs/react';
import { Transition } from '@headlessui/react';
const DropDownContext = createContext();
const Dropdown = ({ children }) => {
const [open, setOpen] = useState(false);
const toggleOpen = () => {
setOpen((previousState) => !previousState);
};
return (
<DropDownContext.Provider value={{ open, setOpen, toggleOpen }}>
<div className="relative">{children}</div>
</DropDownContext.Provider>
);
};
const Trigger = ({ children }) => {
const { open, setOpen, toggleOpen } = useContext(DropDownContext);
return (
<>
<div onClick={toggleOpen}>{children}</div>
{open && <div className="fixed inset-0 z-40" onClick={() => setOpen(false)}></div>}
</>
);
};
const Content = ({ align = 'right', width = '48', contentClasses = 'py-1 bg-white', children }) => {
const { open, setOpen } = useContext(DropDownContext);
let alignmentClasses = 'origin-top';
if (align === 'left') {
alignmentClasses = 'ltr:origin-top-left rtl:origin-top-right start-0';
} else if (align === 'right') {
alignmentClasses = 'ltr:origin-top-right rtl:origin-top-left end-0';
}
let widthClasses = '';
if (width === '48') {
widthClasses = 'w-48';
}
return (
<>
<Transition
as={Fragment}
show={open}
enter="transition ease-out duration-200"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div
className={`absolute z-50 mt-2 rounded-md shadow-lg ${alignmentClasses} ${widthClasses}`}
onClick={() => setOpen(false)}
>
<div className={`rounded-md ring-1 ring-black ring-opacity-5 ` + contentClasses}>{children}</div>
</div>
</Transition>
</>
);
};
const DropdownLink = ({ className = '', children, ...props }) => {
return (
<Link
{...props}
className={
'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out ' +
className
}
>
{children}
</Link>
);
};
Dropdown.Trigger = Trigger;
Dropdown.Content = Content;
Dropdown.Link = DropdownLink;
export default Dropdown;

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

@ -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

@ -0,0 +1,318 @@
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({});
const [selectedPayments, setSelectedPayments] = useState([]);
const [paymentDetails, setPaymentDetails] = useState({});
useEffect(() => {
// console.log(initialData)
setFormData(initialData || {});
setSelectedPayments([]);
setPaymentDetails({});
}, [initialData]);
const handleChange = (e) => {
if (e.target.type === "file") {
setFormData({ ...formData, [e.target.name]: e.target.files[0] });
} else {
setFormData({ ...formData, [e.target.name]: e.target.value });
}
};
// console.log(initialData)
const handlePaymentChange = (e) => {
const typeId = e.target.value;
console.log(typeId)
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,
[typeId]: {
range: 1,
nominal,
penalty,
amount: nominal + penalty,
},
});
}
};
const handleRangeChange = (paymentType, newRange) => {
setPaymentDetails((prevDetails) => {
const validRange = newRange && !isNaN(newRange) ? Math.max(1, Number(newRange)) : 1;
const currentDetails = prevDetails[paymentType] || { nominal: 0, penalty: 0 };
const newAmount = (currentDetails.nominal + currentDetails.penalty) * validRange;
return {
...prevDetails,
[paymentType]: {
...currentDetails,
range: validRange,
amount: newAmount,
},
};
});
};
const handleRemovePayment = (paymentType) => {
setSelectedPayments(selectedPayments.filter((p) => p !== paymentType));
const newDetails = { ...paymentDetails };
delete newDetails[paymentType];
setPaymentDetails(newDetails);
};
const handleSubmit = (e) => {
e.preventDefault();
const formDataObj = new FormData();
Object.keys(formData).forEach((key) => {
if (key === 'foto' && !(formData[key] instanceof File)) {
return;
}
formDataObj.append(key, formData[key]);
});
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}`;
Inertia.post(url, formDataObj, {
forceFormData: true,
onError: (errors) => setErrors(errors),
onSuccess: () => {
document.getElementById('modal_input').checked = false;
setFormData({});
setErrors({});
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 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" 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-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>
))}
</select>
) : 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="password"
name={field}
onChange={handleChange}
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={type}
name={field}
value={formData[field] || ""}
onChange={handleChange}
readOnly={readOnly}
className="input input-info focus:outline-none focus:ring-1 ring-info w-full"
placeholder={`Masukkan ${field.replace("_", " ")}`}
/>
)}
{errors[field] && (
<p className="text-red-500 text-sm mt-1">{errors[field]}</p>
)}
</div>
);
})}
{showPayments && (
<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="border rounded-lg p-4 relative bg-gray-50 shadow-sm"
>
<button
type="button"
onClick={() => handleRemovePayment(paymentType)}
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="form-control mb-2">
<label className="label font-semibold">
{options.payment_type[paymentType]} - Range Bulan
</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="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="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="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-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>
</div>
</div>
);
};
export default ModalInput

View File

@ -0,0 +1,18 @@
import { Link } from '@inertiajs/react';
export default function NavLink({ active = false, className = '', children, ...props }) {
return (
<Link
{...props}
className={
'inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium leading-5 transition duration-150 ease-in-out focus:outline-none ' +
(active
? 'border-indigo-400 text-gray-900 focus:border-indigo-700 '
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:text-gray-700 focus:border-gray-300 ') +
className
}
>
{children}
</Link>
);
}

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

@ -0,0 +1,16 @@
import { Link } from '@inertiajs/react';
export default function ResponsiveNavLink({ active = false, className = '', children, ...props }) {
return (
<Link
{...props}
className={`w-full flex items-start ps-3 pe-4 py-2 border-l-4 ${
active
? 'border-indigo-400 text-indigo-700 bg-indigo-50 focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700'
: 'border-transparent text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300'
} text-base font-medium focus:outline-none transition duration-150 ease-in-out ${className}`}
>
{children}
</Link>
);
}

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