From 0a1d0a9c552f6824325868def132fecbfeaed4d4e0afb5c50ca3bdaab5ded602 Mon Sep 17 00:00:00 2001 From: Abhishek Mali Date: Fri, 5 Dec 2025 17:16:02 +0530 Subject: [PATCH] staff update --- .../Controllers/Admin/AdminAuthController.php | 44 +- .../Admin/AdminOrderController.php | 489 ++++++++++-------- .../Admin/AdminStaffController.php | 179 +++++++ app/Models/Admin.php | 29 +- app/Models/Staff.php | 81 +++ app/Providers/AuthServiceProvider.php | 40 ++ bootstrap/providers.php | 1 + composer.json | 3 +- composer.lock | 85 ++- config/auth.php | 5 + config/permission.php | 202 ++++++++ ..._12_04_070257_create_permission_tables.php | 134 +++++ .../2025_12_04_071300_create_staff_table.php | 50 ++ ...25930_add_staff_fields_to_admins_table.php | 41 ++ ...1613_alter_role_column_in_admins_table.php | 20 + database/seeders/PermissionSeeder.php | 103 ++++ public/invoices/invoice-INV-2025-000017.pdf | Bin 41948 -> 62556 bytes resources/views/admin/account.blade.php | 265 ++++++---- resources/views/admin/customers.blade.php | 2 + resources/views/admin/dashboard.blade.php | 9 +- resources/views/admin/invoice_edit.blade.php | 14 +- resources/views/admin/layouts/app.blade.php | 96 +++- resources/views/admin/login.blade.php | 36 +- resources/views/admin/orders_show.blade.php | 8 +- resources/views/admin/shipments.blade.php | 63 ++- resources/views/admin/staff/create.blade.php | 187 +++++++ resources/views/admin/staff/edit.blade.php | 156 ++++++ resources/views/admin/staff/index.blade.php | 65 +++ routes/web.php | 26 +- 29 files changed, 2001 insertions(+), 432 deletions(-) create mode 100644 app/Http/Controllers/Admin/AdminStaffController.php create mode 100644 app/Models/Staff.php create mode 100644 app/Providers/AuthServiceProvider.php create mode 100644 config/permission.php create mode 100644 database/migrations/2025_12_04_070257_create_permission_tables.php create mode 100644 database/migrations/2025_12_04_071300_create_staff_table.php create mode 100644 database/migrations/2025_12_04_125930_add_staff_fields_to_admins_table.php create mode 100644 database/migrations/2025_12_04_131613_alter_role_column_in_admins_table.php create mode 100644 database/seeders/PermissionSeeder.php create mode 100644 resources/views/admin/staff/create.blade.php create mode 100644 resources/views/admin/staff/edit.blade.php create mode 100644 resources/views/admin/staff/index.blade.php diff --git a/app/Http/Controllers/Admin/AdminAuthController.php b/app/Http/Controllers/Admin/AdminAuthController.php index 109f4a5..3b664d9 100644 --- a/app/Http/Controllers/Admin/AdminAuthController.php +++ b/app/Http/Controllers/Admin/AdminAuthController.php @@ -5,50 +5,52 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; -use App\Models\Admin; - - class AdminAuthController extends Controller { - /** - * Show the admin login page - */ public function showLoginForm() { return view('admin.login'); } - /** - * Handle admin login - */ public function login(Request $request) { $request->validate([ - 'email' => 'required|email', + 'login' => 'required', 'password' => 'required|string|min:6', ]); - // Try to log in using the 'admin' guard - if (Auth::guard('admin')->attempt($request->only('email', 'password'))) { - return redirect()->route('admin.dashboard')->with('success', 'Welcome back, Admin!'); + $loginInput = $request->input('login'); + + if (filter_var($loginInput, FILTER_VALIDATE_EMAIL)) { + $field = 'email'; + } elseif (preg_match('/^EMP\d+$/i', $loginInput)) { + $field = 'employee_id'; + } else { + $field = 'username'; } - return back()->withErrors(['email' => 'Invalid email or password.']); + $credentials = [ + $field => $loginInput, + 'password' => $request->password, + ]; + + // attempt login + if (Auth::guard('admin')->attempt($credentials)) { + $request->session()->regenerate(); + $user = Auth::guard('admin')->user(); + + return redirect()->route('admin.dashboard')->with('success', 'Welcome back, ' . $user->name . '!'); + } + + return back()->withErrors(['login' => 'Invalid login credentials.']); } - /** - * Logout admin - */ public function logout(Request $request) { Auth::guard('admin')->logout(); - - // Destroy the session completely $request->session()->invalidate(); $request->session()->regenerateToken(); - return redirect()->route('admin.login')->with('success', 'Logged out successfully.'); } } diff --git a/app/Http/Controllers/Admin/AdminOrderController.php b/app/Http/Controllers/Admin/AdminOrderController.php index 697af83..245a6c9 100644 --- a/app/Http/Controllers/Admin/AdminOrderController.php +++ b/app/Http/Controllers/Admin/AdminOrderController.php @@ -148,6 +148,7 @@ class AdminOrderController extends Controller // recalc totals and save to order $this->recalcTotals($order); + $this->updateInvoiceFromOrder($order); // <-- NEW return redirect()->back()->with('success', 'Item added and totals updated.'); } @@ -164,6 +165,8 @@ class AdminOrderController extends Controller // recalc totals $this->recalcTotals($order); + $this->updateInvoiceFromOrder($order); + return redirect()->back()->with('success', 'Item deleted and totals updated.'); } @@ -180,6 +183,8 @@ class AdminOrderController extends Controller // recalc totals $this->recalcTotals($order); + $this->updateInvoiceFromOrder($order); + return redirect()->back()->with('success', 'Item restored and totals updated.'); } @@ -383,79 +388,79 @@ class AdminOrderController extends Controller return view('admin.orders', compact('orders')); } -// inside AdminOrderController + // inside AdminOrderController -private function buildOrdersQueryFromRequest(Request $request) -{ - $query = Order::with(['markList', 'invoice', 'shipments']); + private function buildOrdersQueryFromRequest(Request $request) + { + $query = Order::with(['markList', 'invoice', 'shipments']); - // Search across order_id, markList.company_name, markList.customer_id, invoice.invoice_number - if ($request->filled('search')) { - $search = $request->search; - $query->where(function($q) use ($search) { - $q->where('order_id', 'like', "%{$search}%") - ->orWhereHas('markList', function($q2) use ($search) { - $q2->where('company_name', 'like', "%{$search}%") - ->orWhere('customer_id', 'like', "%{$search}%"); - }) - ->orWhereHas('invoice', function($q3) use ($search) { - $q3->where('invoice_number', 'like', "%{$search}%"); - }); - }); + // Search across order_id, markList.company_name, markList.customer_id, invoice.invoice_number + if ($request->filled('search')) { + $search = $request->search; + $query->where(function($q) use ($search) { + $q->where('order_id', 'like', "%{$search}%") + ->orWhereHas('markList', function($q2) use ($search) { + $q2->where('company_name', 'like', "%{$search}%") + ->orWhere('customer_id', 'like', "%{$search}%"); + }) + ->orWhereHas('invoice', function($q3) use ($search) { + $q3->where('invoice_number', 'like', "%{$search}%"); + }); + }); + } + + // Invoice status filter + if ($request->filled('status')) { + $query->whereHas('invoice', function($q) use ($request) { + $q->where('status', $request->status); + }); + } + + // Shipment status filter + if ($request->filled('shipment')) { + $query->whereHas('shipments', function($q) use ($request) { + $q->where('status', $request->shipment); + }); + } + + // optional ordering + $query->latest('id'); + + return $query; } - // Invoice status filter - if ($request->filled('status')) { - $query->whereHas('invoice', function($q) use ($request) { - $q->where('status', $request->status); - }); + public function downloadPdf(Request $request) + { + // Build same filtered query used for table + $query = $this->buildOrdersQueryFromRequest($request); + + $orders = $query->get(); + + // optional: pass filters to view for header + $filters = [ + 'search' => $request->search ?? null, + 'status' => $request->status ?? null, + 'shipment' => $request->shipment ?? null, + ]; + + $pdf = PDF::loadView('admin.orders.pdf', compact('orders', 'filters')) + ->setPaper('a4', 'landscape'); // adjust if needed + + $fileName = 'orders-report' + . ($filters['status'] ? "-{$filters['status']}" : '') + . '-' . date('Y-m-d') . '.pdf'; + + return $pdf->download($fileName); } - // Shipment status filter - if ($request->filled('shipment')) { - $query->whereHas('shipments', function($q) use ($request) { - $q->where('status', $request->shipment); - }); + public function downloadExcel(Request $request) + { + // pass request to OrdersExport which will build Filtered query internally + return Excel::download(new OrdersExport($request), 'orders-report-' . date('Y-m-d') . '.xlsx'); } - // optional ordering - $query->latest('id'); - return $query; -} - -public function downloadPdf(Request $request) -{ - // Build same filtered query used for table - $query = $this->buildOrdersQueryFromRequest($request); - - $orders = $query->get(); - - // optional: pass filters to view for header - $filters = [ - 'search' => $request->search ?? null, - 'status' => $request->status ?? null, - 'shipment' => $request->shipment ?? null, - ]; - - $pdf = PDF::loadView('admin.orders.pdf', compact('orders', 'filters')) - ->setPaper('a4', 'landscape'); // adjust if needed - - $fileName = 'orders-report' - . ($filters['status'] ? "-{$filters['status']}" : '') - . '-' . date('Y-m-d') . '.pdf'; - - return $pdf->download($fileName); -} - -public function downloadExcel(Request $request) -{ - // pass request to OrdersExport which will build Filtered query internally - return Excel::download(new OrdersExport($request), 'orders-report-' . date('Y-m-d') . '.xlsx'); -} - - -public function addTempItem(Request $request) + public function addTempItem(Request $request) { // Validate item fields $item = $request->validate([ @@ -509,184 +514,228 @@ public function addTempItem(Request $request) // ------------------------------------------------------------------------- public function finishOrder(Request $request) -{ - $request->validate([ - 'mark_no' => 'required', - 'origin' => 'required', - 'destination' => 'required', - ]); + { + $request->validate([ + 'mark_no' => 'required', + 'origin' => 'required', + 'destination' => 'required', + ]); - $items = session('temp_order_items', []); + $items = session('temp_order_items', []); - if (empty($items)) { - return redirect()->to(route('admin.orders.index') . '#createOrderForm') - ->with('error', 'Add at least one item before finishing.'); - } + if (empty($items)) { + return redirect()->to(route('admin.orders.index') . '#createOrderForm') + ->with('error', 'Add at least one item before finishing.'); + } - // ======================= - // GENERATE ORDER ID - // ======================= - $year = date('y'); - $prefix = "KNT-$year-"; + // ======================= + // GENERATE ORDER ID + // ======================= + $year = date('y'); + $prefix = "KNT-$year-"; - $lastOrder = Order::latest('id')->first(); - $nextNumber = $lastOrder ? intval(substr($lastOrder->order_id, -8)) + 1 : 1; + $lastOrder = Order::latest('id')->first(); + $nextNumber = $lastOrder ? intval(substr($lastOrder->order_id, -8)) + 1 : 1; - $orderId = $prefix . str_pad($nextNumber, 8, '0', STR_PAD_LEFT); + $orderId = $prefix . str_pad($nextNumber, 8, '0', STR_PAD_LEFT); - // ======================= - // TOTAL SUMS - // ======================= - $total_ctn = array_sum(array_column($items, 'ctn')); - $total_qty = array_sum(array_column($items, 'qty')); - $total_ttl_qty = array_sum(array_column($items, 'ttl_qty')); - $total_amount = array_sum(array_column($items, 'ttl_amount')); - $total_cbm = array_sum(array_column($items, 'cbm')); - $total_ttl_cbm = array_sum(array_column($items, 'ttl_cbm')); - $total_kg = array_sum(array_column($items, 'kg')); - $total_ttl_kg = array_sum(array_column($items, 'ttl_kg')); + // ======================= + // TOTAL SUMS + // ======================= + $total_ctn = array_sum(array_column($items, 'ctn')); + $total_qty = array_sum(array_column($items, 'qty')); + $total_ttl_qty = array_sum(array_column($items, 'ttl_qty')); + $total_amount = array_sum(array_column($items, 'ttl_amount')); + $total_cbm = array_sum(array_column($items, 'cbm')); + $total_ttl_cbm = array_sum(array_column($items, 'ttl_cbm')); + $total_kg = array_sum(array_column($items, 'kg')); + $total_ttl_kg = array_sum(array_column($items, 'ttl_kg')); - // ======================= - // CREATE ORDER - // ======================= - $order = Order::create([ - 'order_id' => $orderId, - 'mark_no' => $request->mark_no, - 'origin' => $request->origin, - 'destination' => $request->destination, - 'ctn' => $total_ctn, - 'qty' => $total_qty, - 'ttl_qty' => $total_ttl_qty, - 'ttl_amount' => $total_amount, - 'cbm' => $total_cbm, - 'ttl_cbm' => $total_ttl_cbm, - 'kg' => $total_kg, - 'ttl_kg' => $total_ttl_kg, - 'status' => 'pending', - ]); + // ======================= + // CREATE ORDER + // ======================= + $order = Order::create([ + 'order_id' => $orderId, + 'mark_no' => $request->mark_no, + 'origin' => $request->origin, + 'destination' => $request->destination, + 'ctn' => $total_ctn, + 'qty' => $total_qty, + 'ttl_qty' => $total_ttl_qty, + 'ttl_amount' => $total_amount, + 'cbm' => $total_cbm, + 'ttl_cbm' => $total_ttl_cbm, + 'kg' => $total_kg, + 'ttl_kg' => $total_ttl_kg, + 'status' => 'pending', + ]); - // SAVE ORDER ITEMS - foreach ($items as $item) { - OrderItem::create([ + // SAVE ORDER ITEMS + foreach ($items as $item) { + OrderItem::create([ + 'order_id' => $order->id, + 'description' => $item['description'], + 'ctn' => $item['ctn'], + 'qty' => $item['qty'], + 'ttl_qty' => $item['ttl_qty'], + 'unit' => $item['unit'], + 'price' => $item['price'], + 'ttl_amount' => $item['ttl_amount'], + 'cbm' => $item['cbm'], + 'ttl_cbm' => $item['ttl_cbm'], + 'kg' => $item['kg'], + 'ttl_kg' => $item['ttl_kg'], + 'shop_no' => $item['shop_no'], + ]); + } + + // ======================= + // INVOICE CREATION START + // ======================= + + // 1. Auto-generate invoice number + $lastInvoice = \App\Models\Invoice::latest()->first(); + $nextInvoice = $lastInvoice ? $lastInvoice->id + 1 : 1; + $invoiceNumber = 'INV-' . date('Y') . '-' . str_pad($nextInvoice, 6, '0', STR_PAD_LEFT); + + // 2. Fetch customer (using mark list → customer_id) + $markList = MarkList::where('mark_no', $order->mark_no)->first(); + $customer = null; + + if ($markList && $markList->customer_id) { + $customer = \App\Models\User::where('customer_id', $markList->customer_id)->first(); + } + + // 3. Create Invoice Record + $invoice = \App\Models\Invoice::create([ 'order_id' => $order->id, - 'description' => $item['description'], - 'ctn' => $item['ctn'], - 'qty' => $item['qty'], - 'ttl_qty' => $item['ttl_qty'], - 'unit' => $item['unit'], - 'price' => $item['price'], - 'ttl_amount' => $item['ttl_amount'], - 'cbm' => $item['cbm'], - 'ttl_cbm' => $item['ttl_cbm'], - 'kg' => $item['kg'], - 'ttl_kg' => $item['ttl_kg'], - 'shop_no' => $item['shop_no'], + 'customer_id' => $customer->id ?? null, + 'mark_no' => $order->mark_no, + + 'invoice_number' => $invoiceNumber, + 'invoice_date' => now(), + 'due_date' => now()->addDays(10), + + 'payment_method' => null, + 'reference_no' => null, + 'status' => 'pending', + + 'final_amount' => $total_amount, + 'gst_percent' => 0, + 'gst_amount' => 0, + 'final_amount_with_gst' => $total_amount, + + // snapshot customer fields + 'customer_name' => $customer->customer_name ?? null, + 'company_name' => $customer->company_name ?? null, + 'customer_email' => $customer->email ?? null, + 'customer_mobile' => $customer->mobile_no ?? null, + 'customer_address' => $customer->address ?? null, + 'pincode' => $customer->pincode ?? null, + + 'notes' => null, ]); + + // 4. Clone order items into invoice_items + foreach ($order->items as $item) { + \App\Models\InvoiceItem::create([ + 'invoice_id' => $invoice->id, + 'description' => $item->description, + 'ctn' => $item->ctn, + 'qty' => $item->qty, + 'ttl_qty' => $item->ttl_qty, + 'unit' => $item->unit, + 'price' => $item->price, + 'ttl_amount' => $item->ttl_amount, + 'cbm' => $item->cbm, + 'ttl_cbm' => $item->ttl_cbm, + 'kg' => $item->kg, + 'ttl_kg' => $item->ttl_kg, + 'shop_no' => $item->shop_no, + ]); + } + + // 5. TODO: PDF generation (I will add this later) + $invoice->pdf_path = null; // placeholder for now + $invoice->save(); + + // ======================= + // END INVOICE CREATION + // ======================= + + // CLEAR TEMP DATA + session()->forget(['temp_order_items', 'mark_no', 'origin', 'destination']); + + return redirect()->route('admin.orders.index') + ->with('success', 'Order + Invoice created successfully.'); } - // ======================= - // INVOICE CREATION START - // ======================= - - // 1. Auto-generate invoice number - $lastInvoice = \App\Models\Invoice::latest()->first(); - $nextInvoice = $lastInvoice ? $lastInvoice->id + 1 : 1; - $invoiceNumber = 'INV-' . date('Y') . '-' . str_pad($nextInvoice, 6, '0', STR_PAD_LEFT); - - // 2. Fetch customer (using mark list → customer_id) - $markList = MarkList::where('mark_no', $order->mark_no)->first(); - $customer = null; - - if ($markList && $markList->customer_id) { - $customer = \App\Models\User::where('customer_id', $markList->customer_id)->first(); - } - - // 3. Create Invoice Record - $invoice = \App\Models\Invoice::create([ - 'order_id' => $order->id, - 'customer_id' => $customer->id ?? null, - 'mark_no' => $order->mark_no, - - 'invoice_number' => $invoiceNumber, - 'invoice_date' => now(), - 'due_date' => now()->addDays(10), - - 'payment_method' => null, - 'reference_no' => null, - 'status' => 'pending', - - 'final_amount' => $total_amount, - 'gst_percent' => 0, - 'gst_amount' => 0, - 'final_amount_with_gst' => $total_amount, - - // snapshot customer fields - 'customer_name' => $customer->customer_name ?? null, - 'company_name' => $customer->company_name ?? null, - 'customer_email' => $customer->email ?? null, - 'customer_mobile' => $customer->mobile_no ?? null, - 'customer_address' => $customer->address ?? null, - 'pincode' => $customer->pincode ?? null, - - 'notes' => null, - ]); - - // 4. Clone order items into invoice_items - foreach ($order->items as $item) { - \App\Models\InvoiceItem::create([ - 'invoice_id' => $invoice->id, - 'description' => $item->description, - 'ctn' => $item->ctn, - 'qty' => $item->qty, - 'ttl_qty' => $item->ttl_qty, - 'unit' => $item->unit, - 'price' => $item->price, - 'ttl_amount' => $item->ttl_amount, - 'cbm' => $item->cbm, - 'ttl_cbm' => $item->ttl_cbm, - 'kg' => $item->kg, - 'ttl_kg' => $item->ttl_kg, - 'shop_no' => $item->shop_no, - ]); - } - - // 5. TODO: PDF generation (I will add this later) - $invoice->pdf_path = null; // placeholder for now - $invoice->save(); - - // ======================= - // END INVOICE CREATION - // ======================= - - // CLEAR TEMP DATA - session()->forget(['temp_order_items', 'mark_no', 'origin', 'destination']); - - return redirect()->route('admin.orders.index') - ->with('success', 'Order + Invoice created successfully.'); -} // --------------------------- // ORDER CRUD: update / destroy // --------------------------- - public function updateItem(Request $request, $id) + public function updateItem(Request $request, $id) { $item = OrderItem::findOrFail($id); + $order = $item->order; $item->update([ 'description' => $request->description, - 'ctn' => $request->ctn, - 'qty' => $request->qty, - 'ttl_qty' => $request->ttl_qty, - 'unit' => $request->unit, - 'price' => $request->price, - 'ttl_amount' => $request->ttl_amount, - 'cbm' => $request->cbm, - 'ttl_cbm' => $request->ttl_cbm, - 'kg' => $request->kg, - 'ttl_kg' => $request->ttl_kg, - 'shop_no' => $request->shop_no, + 'ctn' => $request->ctn, + 'qty' => $request->qty, + 'ttl_qty' => $request->ttl_qty, + 'unit' => $request->unit, + 'price' => $request->price, + 'ttl_amount' => $request->ttl_amount, + 'cbm' => $request->cbm, + 'ttl_cbm' => $request->ttl_cbm, + 'kg' => $request->kg, + 'ttl_kg' => $request->ttl_kg, + 'shop_no' => $request->shop_no, ]); - return back()->with('success', 'Item updated successfully!'); + $this->recalcTotals($order); + $this->updateInvoiceFromOrder($order); // <-- NEW + + return back()->with('success', 'Item updated successfully'); + } + + + private function updateInvoiceFromOrder(Order $order) + { + $invoice = Invoice::where('order_id', $order->id)->first(); + + if (!$invoice) { + return; // No invoice exists (should not happen normally) + } + + // Update invoice totals + $invoice->final_amount = $order->ttl_amount; + $invoice->gst_percent = 0; + $invoice->gst_amount = 0; + $invoice->final_amount_with_gst = $order->ttl_amount; + $invoice->save(); + + // Delete old invoice items + InvoiceItem::where('invoice_id', $invoice->id)->delete(); + + // Re-create invoice items from updated order items + foreach ($order->items as $item) { + InvoiceItem::create([ + 'invoice_id' => $invoice->id, + 'description' => $item->description, + 'ctn' => $item->ctn, + 'qty' => $item->qty, + 'ttl_qty' => $item->ttl_qty, + 'unit' => $item->unit, + 'price' => $item->price, + 'ttl_amount' => $item->ttl_amount, + 'cbm' => $item->cbm, + 'ttl_cbm' => $item->ttl_cbm, + 'kg' => $item->kg, + 'ttl_kg' => $item->ttl_kg, + 'shop_no' => $item->shop_no, + ]); + } } } \ No newline at end of file diff --git a/app/Http/Controllers/Admin/AdminStaffController.php b/app/Http/Controllers/Admin/AdminStaffController.php new file mode 100644 index 0000000..020239e --- /dev/null +++ b/app/Http/Controllers/Admin/AdminStaffController.php @@ -0,0 +1,179 @@ +orderBy('id', 'DESC')->get(); + return view('admin.staff.index', compact('staff')); + } + + public function create() + { + $permissions = Permission::where('guard_name', 'admin')->get()->groupBy(function ($p) { + return explode('.', $p->name)[0]; + }); + + return view('admin.staff.create', compact('permissions')); + } + + public function store(Request $request) + { + $request->validate([ + // Personal Info + 'name' => 'required|string|max:255', + 'email' => 'required|email|unique:admins,email', + 'phone' => 'required|string|max:20', + 'emergency_phone' => 'nullable|string|max:20', + 'address' => 'nullable|string|max:255', + + // Professional info + 'role' => 'nullable|string|max:100', + 'department' => 'nullable|string|max:100', + 'designation' => 'nullable|string|max:100', + 'joining_date' => 'nullable|date', + 'status' => 'required|string|in:active,inactive', + 'additional_info' => 'nullable|string', + + // System access + 'username' => 'nullable|string|unique:admins,username', + 'password' => 'required|string|min:6', + + // Permissions + 'permissions' => 'nullable|array', + ]); + + DB::beginTransaction(); + + try { + $admin = Admin::create([ + 'name' => $request->name, + 'email' => $request->email, + 'phone' => $request->phone, + 'emergency_phone' => $request->emergency_phone, + 'address' => $request->address, + + 'role' => $request->role, + 'department' => $request->department, + 'designation' => $request->designation, + 'joining_date' => $request->joining_date, + 'status' => $request->status, + 'additional_info' => $request->additional_info, + + 'username' => $request->username, + 'password' => Hash::make($request->password), + 'type' => 'staff', + ]); + + // Generate EMPLOYEE ID using admin ID (safe) + $employeeId = 'EMP' . str_pad($admin->id, 4, '0', STR_PAD_LEFT); + $admin->update(['employee_id' => $employeeId]); + + // Assign permissions (if any) + if ($request->permissions) { + $admin->givePermissionTo($request->permissions); + } + + DB::commit(); + + return redirect()->route('admin.staff.index') + ->with('success', 'Staff created successfully.'); + + } catch (\Exception $e) { + DB::rollBack(); + return back()->withErrors(['error' => $e->getMessage()]); + } + } + + public function edit($id) + { + $staff = Admin::where('type', 'staff')->findOrFail($id); + + $permissions = Permission::where('guard_name', 'admin')->get()->groupBy(function ($p) { + return explode('.', $p->name)[0]; + }); + + $staffPermissions = $staff->permissions->pluck('name')->toArray(); + + return view('admin.staff.edit', compact('staff', 'permissions', 'staffPermissions')); + } + + public function update(Request $request, $id) + { + $staff = Admin::where('type', 'staff')->findOrFail($id); + + $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|email|unique:admins,email,' . $staff->id, + 'phone' => 'required|string|max:20', + 'emergency_phone' => 'nullable|string|max:20', + 'address' => 'nullable|string|max:255', + + 'role' => 'nullable|string|max:100', + 'department' => 'nullable|string|max:100', + 'designation' => 'nullable|string|max:100', + 'joining_date' => 'nullable|date', + 'status' => 'required|string|in:active,inactive', + 'additional_info' => 'nullable|string', + + 'username' => 'nullable|string|unique:admins,username,' . $staff->id, + 'password' => 'nullable|string|min:6', + + 'permissions' => 'nullable|array', + ]); + + DB::beginTransaction(); + + try { + $staff->update([ + 'name' => $request->name, + 'email' => $request->email, + 'phone' => $request->phone, + 'emergency_phone' => $request->emergency_phone, + 'address' => $request->address, + + 'role' => $request->role, + 'department' => $request->department, + 'designation' => $request->designation, + 'joining_date' => $request->joining_date, + 'status' => $request->status, + 'additional_info' => $request->additional_info, + + 'username' => $request->username, + ]); + + if ($request->password) { + $staff->update(['password' => Hash::make($request->password)]); + } + + $staff->syncPermissions($request->permissions ?? []); + + DB::commit(); + + return redirect()->route('admin.staff.index') + ->with('success', 'Staff updated successfully.'); + + } catch (\Exception $e) { + DB::rollBack(); + return back()->withErrors(['error' => $e->getMessage()]); + } + } + + public function destroy($id) + { + $staff = Admin::where('type', 'staff')->findOrFail($id); + $staff->delete(); + + return redirect()->route('admin.staff.index') + ->with('success', 'Staff removed successfully.'); + } +} diff --git a/app/Models/Admin.php b/app/Models/Admin.php index a107900..118f371 100644 --- a/app/Models/Admin.php +++ b/app/Models/Admin.php @@ -1,22 +1,43 @@ attributes['password'] = Hash::make($value); + } else { + $this->attributes['password'] = $value; + } + } + + public function getDisplayNameAttribute() + { + return "{$this->name}"; + } } diff --git a/app/Models/Staff.php b/app/Models/Staff.php new file mode 100644 index 0000000..77327d8 --- /dev/null +++ b/app/Models/Staff.php @@ -0,0 +1,81 @@ + 'date', + ]; + + /** + * Mutator: automatically hash password when set. + * Accepts plain text and hashes it with bcrypt. + */ + public function setPasswordAttribute($value) + { + if (empty($value)) { + return; + } + + // If already hashed (starts with $2y$), don't double-hash + if (Hash::needsRehash($value)) { + $this->attributes['password'] = Hash::make($value); + } else { + $this->attributes['password'] = $value; + } + } + + /** + * Optional helper to get display name (useful in views/logs). + */ + public function getDisplayNameAttribute() + { + return $this->name . ' (' . $this->employee_id . ')'; + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php new file mode 100644 index 0000000..b5ea04c --- /dev/null +++ b/app/Providers/AuthServiceProvider.php @@ -0,0 +1,40 @@ + + */ + protected $policies = [ + // 'App\Models\Model' => 'App\Policies\ModelPolicy', + ]; + + /** + * Register any authentication / authorization services. + */ + public function boot() + { + $this->registerPolicies(); + + // SUPER ADMIN bypass + Gate::before(function ($user, $ability) { + if ($user->hasRole('super-admin')) { + return true; + } + }); + + // ADMIN bypass + Gate::before(function ($user, $ability) { + if ($user->hasRole('admin')) { + return true; + } + }); + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index f2b5dec..8e6dce1 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -3,4 +3,5 @@ return [ App\Providers\AppServiceProvider::class, App\Providers\RouteServiceProvider::class, + App\Providers\AuthServiceProvider::class, ]; diff --git a/composer.json b/composer.json index 1545c24..a38437b 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,8 @@ "laravel/tinker": "^2.10.1", "maatwebsite/excel": "^1.1", "mpdf/mpdf": "^8.2", - "php-open-source-saver/jwt-auth": "2.8" + "php-open-source-saver/jwt-auth": "2.8", + "spatie/laravel-permission": "^6.23" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index a94cb96..bf0a4be 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6a2ec43d7e96d38cacfc2f9e1ae5cb9e", + "content-hash": "0ed807863dbf93f06565547ce4303f47", "packages": [ { "name": "barryvdh/laravel-dompdf", @@ -4364,6 +4364,89 @@ ], "time": "2025-08-05T09:57:14+00:00" }, + { + "name": "spatie/laravel-permission", + "version": "6.23.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-permission.git", + "reference": "9e41247bd512b1e6c229afbc1eb528f7565ae3bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/9e41247bd512b1e6c229afbc1eb528f7565ae3bb", + "reference": "9e41247bd512b1e6c229afbc1eb528f7565ae3bb", + "shasum": "" + }, + "require": { + "illuminate/auth": "^8.12|^9.0|^10.0|^11.0|^12.0", + "illuminate/container": "^8.12|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^8.12|^9.0|^10.0|^11.0|^12.0", + "illuminate/database": "^8.12|^9.0|^10.0|^11.0|^12.0", + "php": "^8.0" + }, + "require-dev": { + "laravel/passport": "^11.0|^12.0", + "laravel/pint": "^1.0", + "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0", + "phpunit/phpunit": "^9.4|^10.1|^11.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Permission\\PermissionServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "6.x-dev", + "dev-master": "6.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Permission\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Permission handling for Laravel 8.0 and up", + "homepage": "https://github.com/spatie/laravel-permission", + "keywords": [ + "acl", + "laravel", + "permission", + "permissions", + "rbac", + "roles", + "security", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-permission/issues", + "source": "https://github.com/spatie/laravel-permission/tree/6.23.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-11-03T20:16:13+00:00" + }, { "name": "symfony/clock", "version": "v7.4.0", diff --git a/config/auth.php b/config/auth.php index 1ced308..3015769 100644 --- a/config/auth.php +++ b/config/auth.php @@ -85,6 +85,11 @@ return [ // 'driver' => 'database', // 'table' => 'users', // ], + 'staff' => [ + 'driver' => 'eloquent', + 'model' => App\Models\Staff::class, + ], + ], /* diff --git a/config/permission.php b/config/permission.php new file mode 100644 index 0000000..b6c88e2 --- /dev/null +++ b/config/permission.php @@ -0,0 +1,202 @@ + [ + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * Eloquent model should be used to retrieve your permissions. Of course, it + * is often just the "Permission" model but you may use whatever you like. + * + * The model you want to use as a Permission model needs to implement the + * `Spatie\Permission\Contracts\Permission` contract. + */ + + 'permission' => Spatie\Permission\Models\Permission::class, + + /* + * When using the "HasRoles" trait from this package, we need to know which + * Eloquent model should be used to retrieve your roles. Of course, it + * is often just the "Role" model but you may use whatever you like. + * + * The model you want to use as a Role model needs to implement the + * `Spatie\Permission\Contracts\Role` contract. + */ + + 'role' => Spatie\Permission\Models\Role::class, + + ], + + 'table_names' => [ + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'roles' => 'roles', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your permissions. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'permissions' => 'permissions', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your models permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_permissions' => 'model_has_permissions', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your models roles. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_roles' => 'model_has_roles', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'role_has_permissions' => 'role_has_permissions', + ], + + 'column_names' => [ + /* + * Change this if you want to name the related pivots other than defaults + */ + 'role_pivot_key' => null, // default 'role_id', + 'permission_pivot_key' => null, // default 'permission_id', + + /* + * Change this if you want to name the related model primary key other than + * `model_id`. + * + * For example, this would be nice if your primary keys are all UUIDs. In + * that case, name this `model_uuid`. + */ + + 'model_morph_key' => 'model_id', + + /* + * Change this if you want to use the teams feature and your related model's + * foreign key is other than `team_id`. + */ + + 'team_foreign_key' => 'team_id', + ], + + /* + * When set to true, the method for checking permissions will be registered on the gate. + * Set this to false if you want to implement custom logic for checking permissions. + */ + + 'register_permission_check_method' => true, + + /* + * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered + * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated + * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it. + */ + 'register_octane_reset_listener' => false, + + /* + * Events will fire when a role or permission is assigned/unassigned: + * \Spatie\Permission\Events\RoleAttached + * \Spatie\Permission\Events\RoleDetached + * \Spatie\Permission\Events\PermissionAttached + * \Spatie\Permission\Events\PermissionDetached + * + * To enable, set to true, and then create listeners to watch these events. + */ + 'events_enabled' => false, + + /* + * Teams Feature. + * When set to true the package implements teams using the 'team_foreign_key'. + * If you want the migrations to register the 'team_foreign_key', you must + * set this to true before doing the migration. + * If you already did the migration then you must make a new migration to also + * add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions' + * (view the latest version of this package's migration file) + */ + + 'teams' => false, + + /* + * The class to use to resolve the permissions team id + */ + 'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class, + + /* + * Passport Client Credentials Grant + * When set to true the package will use Passports Client to check permissions + */ + + 'use_passport_client_credentials' => false, + + /* + * When set to true, the required permission names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_permission_in_exception' => false, + + /* + * When set to true, the required role names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_role_in_exception' => false, + + /* + * By default wildcard permission lookups are disabled. + * See documentation to understand supported syntax. + */ + + 'enable_wildcard_permission' => false, + + /* + * The class to use for interpreting wildcard permissions. + * If you need to modify delimiters, override the class and specify its name here. + */ + // 'wildcard_permission' => Spatie\Permission\WildcardPermission::class, + + /* Cache-specific settings */ + + 'cache' => [ + + /* + * By default all permissions are cached for 24 hours to speed up performance. + * When permissions or roles are updated the cache is flushed automatically. + */ + + 'expiration_time' => \DateInterval::createFromDateString('24 hours'), + + /* + * The cache key used to store all permissions. + */ + + 'key' => 'spatie.permission.cache', + + /* + * You may optionally indicate a specific cache driver to use for permission and + * role caching using any of the `store` drivers listed in the cache.php config + * file. Using 'default' here means to use the `default` set in cache.php. + */ + + 'store' => 'default', + ], +]; diff --git a/database/migrations/2025_12_04_070257_create_permission_tables.php b/database/migrations/2025_12_04_070257_create_permission_tables.php new file mode 100644 index 0000000..9eebf62 --- /dev/null +++ b/database/migrations/2025_12_04_070257_create_permission_tables.php @@ -0,0 +1,134 @@ +engine('InnoDB'); + $table->bigIncrements('id'); // permission id + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); + $table->timestamps(); + + $table->unique(['name', 'guard_name']); + }); + + Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) { + // $table->engine('InnoDB'); + $table->bigIncrements('id'); // role id + if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing + $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); + $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); + } + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); + $table->timestamps(); + if ($teams || config('permission.testing')) { + $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']); + } else { + $table->unique(['name', 'guard_name']); + } + }); + + Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { + $table->unsignedBigInteger($pivotPermission); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index'); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } else { + $table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } + + }); + + Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { + $table->unsignedBigInteger($pivotRole); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } else { + $table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } + }); + + Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) { + $table->unsignedBigInteger($pivotPermission); + $table->unsignedBigInteger($pivotRole); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->onDelete('cascade'); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->onDelete('cascade'); + + $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary'); + }); + + app('cache') + ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) + ->forget(config('permission.cache.key')); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $tableNames = config('permission.table_names'); + + throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.'); + + Schema::drop($tableNames['role_has_permissions']); + Schema::drop($tableNames['model_has_roles']); + Schema::drop($tableNames['model_has_permissions']); + Schema::drop($tableNames['roles']); + Schema::drop($tableNames['permissions']); + } +}; diff --git a/database/migrations/2025_12_04_071300_create_staff_table.php b/database/migrations/2025_12_04_071300_create_staff_table.php new file mode 100644 index 0000000..f4e0c46 --- /dev/null +++ b/database/migrations/2025_12_04_071300_create_staff_table.php @@ -0,0 +1,50 @@ +id(); + + // Personal Information + $table->string('employee_id')->unique(); + $table->string('name'); + $table->string('email')->unique(); + $table->string('phone'); + $table->string('emergency_phone')->nullable(); + $table->text('address')->nullable(); + + // Professional Information + $table->string('role')->nullable(); // Job title + $table->string('department')->nullable(); + $table->string('designation')->nullable(); + $table->date('joining_date')->nullable(); + $table->string('status')->default('active'); // active/inactive + $table->text('additional_info')->nullable(); + + // System Access + $table->string('username')->unique(); + $table->string('password'); + + $table->softDeletes(); + $table->timestamps(); + }); + } + + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('staff'); + } +}; diff --git a/database/migrations/2025_12_04_125930_add_staff_fields_to_admins_table.php b/database/migrations/2025_12_04_125930_add_staff_fields_to_admins_table.php new file mode 100644 index 0000000..7cbf2b8 --- /dev/null +++ b/database/migrations/2025_12_04_125930_add_staff_fields_to_admins_table.php @@ -0,0 +1,41 @@ +string('employee_id')->unique()->nullable(); + $table->string('phone')->nullable(); + $table->string('emergency_phone')->nullable(); + $table->text('address')->nullable(); + + $table->string('department')->nullable(); + $table->string('designation')->nullable(); + $table->date('joining_date')->nullable(); + $table->enum('status', ['active','inactive'])->default('active'); + $table->text('additional_info')->nullable(); + + $table->string('username')->unique()->nullable(); + $table->enum('type', ['admin','staff'])->default('staff'); +}); + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('admins', function (Blueprint $table) { + // + }); + } +}; diff --git a/database/migrations/2025_12_04_131613_alter_role_column_in_admins_table.php b/database/migrations/2025_12_04_131613_alter_role_column_in_admins_table.php new file mode 100644 index 0000000..bf9e88f --- /dev/null +++ b/database/migrations/2025_12_04_131613_alter_role_column_in_admins_table.php @@ -0,0 +1,20 @@ +string('role')->nullable()->change(); // <-- Fix problem + }); + } + + public function down() + { + Schema::table('admins', function (Blueprint $table) { + $table->enum('role', ['admin', 'super-admin'])->nullable()->change(); + }); + } +}; diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php new file mode 100644 index 0000000..aed64eb --- /dev/null +++ b/database/seeders/PermissionSeeder.php @@ -0,0 +1,103 @@ + $permission, 'guard_name' => 'admin'] + ); + } + + // ------------------------------------------------------ + // ROLES + // ------------------------------------------------------ + + // Create super-admin role + $superAdminRole = Role::firstOrCreate( + ['name' => 'super-admin', 'guard_name' => 'admin'] + ); + + // Create admin role + $adminRole = Role::firstOrCreate( + ['name' => 'admin', 'guard_name' => 'admin'] + ); + + // ------------------------------------------------------ + // ASSIGN ALL PERMISSIONS TO BOTH ROLES + // ------------------------------------------------------ + + $allPermissions = Permission::where('guard_name', 'admin')->get(); + + $superAdminRole->syncPermissions($allPermissions); + $adminRole->syncPermissions($allPermissions); + } +} diff --git a/public/invoices/invoice-INV-2025-000017.pdf b/public/invoices/invoice-INV-2025-000017.pdf index ea85217b1ea5651199a0f0f8e1b6a8f0574e5275cdf16416d0130506a5d2cbef..23bd0045d49024e3e60e924eae7e719702eba5e92446091231cdb9343c3ed560 100644 GIT binary patch delta 32248 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 AlK=n! delta 8542 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 onRFqE3U3Gp|6`Ltc@sQR!i$i^L - + @can('account.create_order') + + @endcan
@@ -1250,7 +1252,7 @@ tr:hover td{ background:#fbfdff; }
- +
@@ -1304,7 +1306,9 @@ tr:hover td{ background:#fbfdff; }
+ @can('account.add_installment') + @endcan
@@ -1388,7 +1392,9 @@ tr:hover td{ background:#fbfdff; }
- + @can('account.add_installment') + + @endcan
@@ -1559,6 +1565,14 @@ tr:hover td{ background:#fbfdff; } + + + + +@endsection diff --git a/resources/views/admin/staff/edit.blade.php b/resources/views/admin/staff/edit.blade.php new file mode 100644 index 0000000..d6fc451 --- /dev/null +++ b/resources/views/admin/staff/edit.blade.php @@ -0,0 +1,156 @@ +@extends('admin.layouts.app') + +@section('page-title', 'Account Dashboard') + +@section('content') + + +
+

Edit Staff — {{ $staff->display_name ?? $staff->name }}

+ + @if($errors->any()) +
+ There were some problems with your input: +
    + @foreach($errors->all() as $err) +
  • {{ $err }}
  • + @endforeach +
+
+ @endif + +
+ @csrf + @method('PUT') + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ +
Permissions
+ + @foreach($permissions as $group => $groupPerms) +
+
+
{{ ucfirst($group) }}
+
+ +
+
+ +
+ @foreach($groupPerms as $perm) + + @endforeach +
+
+ @endforeach + +
+ Cancel + +
+
+
+ + +@endsection diff --git a/resources/views/admin/staff/index.blade.php b/resources/views/admin/staff/index.blade.php new file mode 100644 index 0000000..479014d --- /dev/null +++ b/resources/views/admin/staff/index.blade.php @@ -0,0 +1,65 @@ +@extends('admin.layouts.app') + +@section('page-title', 'Account Dashboard') + +@section('content') + + +
+

Staff

+ Add Staff +
+ +
+ @if(session('success')) +
{{ session('success') }}
+ @endif + + + + + + + + + + + + + + + + @forelse($staff as $s) + + + + + + + + + + + @empty + + @endforelse + +
#Employee IDNameEmailPhoneRoleStatusActions
{{ $s->id }}{{ $s->employee_id }}{{ $s->name }}{{ $s->email }}{{ $s->phone }}{{ $s->role ?? '-' }}{{ ucfirst($s->status) }} + Edit +
+ @csrf + @method('DELETE') + +
+
No staff found.
+
+@endsection diff --git a/routes/web.php b/routes/web.php index 18fa2f4..5620968 100644 --- a/routes/web.php +++ b/routes/web.php @@ -10,6 +10,7 @@ use App\Http\Controllers\Admin\AdminInvoiceController; use App\Http\Controllers\Admin\AdminCustomerController; use App\Http\Controllers\Admin\AdminAccountController; use App\Http\Controllers\Admin\AdminReportController; +use App\Http\Controllers\Admin\AdminStaffController; // --------------------------- // Public Front Page @@ -21,19 +22,11 @@ Route::get('/', function () { // --------------------------- // ADMIN LOGIN ROUTES // --------------------------- +// login routes (public) Route::prefix('admin')->group(function () { - - // MUST have route name "login" for session redirect - Route::get('/login', [AdminAuthController::class, 'showLoginForm']) - ->name('admin.login'); - - Route::post('/login', [AdminAuthController::class, 'login']) - ->name('admin.login.submit'); - - Route::post('/logout', [AdminAuthController::class, 'logout']) - ->name('admin.logout'); - - + Route::get('/login', [AdminAuthController::class, 'showLoginForm'])->name('admin.login'); + Route::post('/login', [AdminAuthController::class, 'login'])->name('admin.login.submit'); + Route::post('/logout', [AdminAuthController::class, 'logout'])->name('admin.logout'); }); @@ -295,4 +288,13 @@ Route::prefix('admin') //--------------------------- //Edit Button Route //--------------------------- + // protected admin routes + Route::middleware(['auth:admin']) + ->prefix('admin') + ->name('admin.') + ->group(function () { + + // staff resource + Route::resource('staff', AdminStaffController::class); + });