Compare commits

..

10 Commits

Author SHA256 Message Date
Abhishek Mali
0a65d5f596 staff 2025-12-08 10:17:46 +05:30
Abhishek Mali
0a1d0a9c55 staff update 2025-12-05 17:16:02 +05:30
Abhishek Mali
409a854d7b error fix 2025-12-04 12:08:45 +05:30
Utkarsh Khedkar
4dab96b8d1 Account and Shipment Changes 2025-12-04 11:21:46 +05:30
Utkarsh Khedkar
e7fef314fc Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-03 16:17:36 +05:30
Utkarsh Khedkar
5114357ff2 Account Changes 2025-12-03 16:17:14 +05:30
Utkarsh Khedkar
9b8c50fcec Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-03 11:09:28 +05:30
Utkarsh Khedkar
7a814dff1d Dasshboard Changes 2025-12-03 11:09:12 +05:30
Utkarsh Khedkar
3b24ee860a Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-02 18:12:15 +05:30
Utkarsh Khedkar
2dcd9fe332 Account Changes 2025-12-02 18:11:58 +05:30
37 changed files with 3276 additions and 2603 deletions

View File

@@ -11,13 +11,16 @@ use Illuminate\Support\Facades\DB;
class AdminAccountController extends Controller
{
public function updateEntry(Request $request)
public function updateEntry(Request $request)
{
try {
$data = $request->validate([
'entry_no' => 'required|exists:entries,entry_no',
'description' => 'required|string|max:255',
'order_quantity' => 'required|numeric|min:0',
'entry_no' => 'required|exists:entries,entry_no',
'description' => 'required|string|max:255',
'order_quantity' => 'required|numeric|min:0',
'region' => 'required|string|max:50',
'amount' => 'required|numeric|min:0',
//'payment_status' => 'required|string|max:50',
]);
$entry = Entry::where('entry_no', $data['entry_no'])->first();
@@ -31,6 +34,10 @@ class AdminAccountController extends Controller
$entry->description = $data['description'];
$entry->order_quantity = $data['order_quantity'];
$entry->region = $data['region'];
$entry->amount = $data['amount'];
//$entry->payment_status = $data['payment_status'];
$entry->save();
return response()->json([
@@ -46,36 +53,6 @@ class AdminAccountController extends Controller
}
}
public function deleteEntry(Request $request)
{
try {
$data = $request->validate([
'entry_no' => 'required|exists:entries,entry_no',
]);
$entry = Entry::where('entry_no', $data['entry_no'])->first();
if (!$entry) {
return response()->json([
'success' => false,
'message' => 'Entry not found.',
], 404);
}
$entry->delete();
return response()->json([
'success' => true,
'message' => 'Entry deleted successfully.',
]);
} catch (\Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Server error: '.$e->getMessage(),
], 500);
}
}
/**
* 🚀 1. Get dashboard entries
*/
@@ -96,15 +73,17 @@ public function deleteEntry(Request $request)
*/
public function getAvailableOrders()
{
$orders = Order::whereDoesntHave('entries')
->orderBy('id', 'desc')
->get();
$orders = Order::whereDoesntHave('entries')
->orderBy('id', 'desc')
->get();
return response()->json([
'success' => true,
'orders' => $orders
'orders' => $orders,
]);
}
/**
* 🚀 3. Create new entry
@@ -325,10 +304,9 @@ public function deleteEntry(Request $request)
return DB::transaction(function () use ($data) {
$entry = Entry::where('entry_no', $data['entry_no'])->firstOrFail();
// आधीचे orders काढू नका, फक्त नवीन add करा
$entry->orders()->syncWithoutDetaching($data['order_ids']);
// इथे quantity auto update
$entry->order_quantity = $entry->orders()->count();
$entry->save();
@@ -387,5 +365,35 @@ public function removeOrderFromEntry(Request $request)
]);
});
}
public function deleteEntry(Request $request)
{
try {
$data = $request->validate([
'entry_no' => 'required|exists:entries,entry_no',
]);
$entry = Entry::where('entry_no', $data['entry_no'])->first();
if (!$entry) {
return response()->json([
'success' => false,
'message' => 'Entry not found.',
], 404);
}
$entry->delete();
return response()->json([
'success' => true,
'message' => 'Entry deleted successfully.',
]);
} catch (\Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Server error: '.$e->getMessage(),
], 500);
}
}
}

View File

@@ -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.');
}
}

View File

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

View File

@@ -0,0 +1,179 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use App\Models\Admin;
use Spatie\Permission\Models\Permission;
use Illuminate\Support\Facades\DB;
class AdminStaffController extends Controller
{
public function index()
{
$staff = Admin::where('type', 'staff')->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.');
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Http\Controllers\Admin;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Shipment;
@@ -107,9 +106,6 @@ class ShipmentController extends Controller
'order_qty' => $order->qty,
'order_ttl_qty' => $order->ttl_qty,
'order_ttl_amount' => $order->ttl_amount,
'order_cbm' => $order->cbm,
'order_ttl_cbm' => $order->ttl_cbm,
'order_kg' => $order->kg,
'order_ttl_kg' => $order->ttl_kg,
]);
}
@@ -117,110 +113,22 @@ class ShipmentController extends Controller
return redirect()->back()->with('success', "Shipment $newShipmentId created successfully!");
}
public function edit($id)
{
$shipment = Shipment::with('orders')->findOrFail($id);
return view('admin.shipments.show', [
'shipment' => $shipment,
'orders' => $shipment->orders,
'isViewMode' => false // This is the edit mode
]);
}
/**
* Show shipment details (for modal popup)
*/
public function show($id)
{
$mode = request()->get('mode', 'view');
$shipment = Shipment::with(['items.order'])->findOrFail($id);
// Get orders from shipment items
$orders = collect();
foreach ($shipment->items as $item) {
if ($item->order) {
$orders->push($item->order);
}
}
// Get orders not assigned to any shipment (available orders)
$usedOrderIds = ShipmentItem::pluck('order_id')->toArray();
$availableOrders = Order::whereNotIn('id', $usedOrderIds)->get();
return view('admin.view-shipment', [
'shipment' => $shipment,
'orders' => $orders,
'mode' => $mode,
'availableOrders' => $availableOrders
]);
}
public function addOrders(Request $request, $id)
{
$request->validate([
'order_ids' => 'required|array|min:1'
]);
$shipment = Shipment::findOrFail($id);
$orderIds = $request->order_ids;
DB::beginTransaction();
try {
$orders = Order::whereIn('id', $orderIds)->get();
$addedOrders = [];
// Load full order data from orders table
$orders = Order::whereIn('id',
ShipmentItem::where('shipment_id', $id)->pluck('order_id')
)->get();
foreach ($orders as $order) {
// Prevent duplicates
if (ShipmentItem::where('shipment_id', $shipment->id)->where('order_id', $order->id)->exists()) {
continue;
}
ShipmentItem::create([
'shipment_id' => $shipment->id,
'order_id' => $order->id,
'order_ctn' => $order->ctn,
'order_qty' => $order->qty,
'order_ttl_qty' => $order->ttl_qty,
'order_ttl_amount' => $order->ttl_amount,
'order_cbm' => $order->cbm,
'order_ttl_cbm' => $order->ttl_cbm,
'order_kg' => $order->kg,
'order_ttl_kg' => $order->ttl_kg,
]);
$addedOrders[] = $order;
}
// Recalculate totals
$this->recalculateShipmentTotals($shipment->id);
DB::commit();
$shipment->refresh();
$shipment->load('items.order');
// Get updated orders list
$updatedOrders = collect();
foreach ($shipment->items as $item) {
if ($item->order) {
$updatedOrders->push($item->order);
}
}
return response()->json([
'success' => true,
'message' => 'Orders added to shipment successfully.',
'shipment' => $shipment,
'orders' => $addedOrders
]);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Failed to add orders: ' . $e->getMessage()
], 500);
}
return response()->json([
'shipment' => $shipment,
'orders' => $orders
]);
}
/**
@@ -302,76 +210,19 @@ class ShipmentController extends Controller
->with('success', 'Shipment deleted successfully.');
}
public function removeOrder(Request $request)
public function dummy($id)
{
$request->validate([
'shipment_id' => 'required|exists:shipments,id',
'order_id' => 'required|exists:orders,id'
]);
// Load shipment
$shipment = Shipment::with('orders')->findOrFail($id);
$shipmentId = $request->shipment_id;
$orderId = $request->order_id;
// Dummy data (you can modify anytime)
$dummyData = [
'title' => 'Dummy Shipment Preview',
'generated_on' => now()->format('d M Y h:i A'),
'note' => 'This is dummy shipment information for testing page layout.'
];
// Get order data before deletion
$order = Order::findOrFail($orderId);
// Delete pivot entry
ShipmentItem::where('shipment_id', $shipmentId)
->where('order_id', $orderId)
->delete();
// Recalculate totals
$shipment = $this->recalculateShipmentTotals($shipmentId);
if ($request->ajax() || $request->wantsJson()) {
return response()->json([
'success' => true,
'message' => 'Order removed successfully.',
'order' => $order,
'shipment' => $shipment
]);
}
return back()->with('success', 'Order removed successfully.');
return view('admin.view_shipment', compact('shipment', 'dummyData'));
}
private function recalculateShipmentTotals($shipmentId)
{
$shipment = Shipment::with('items')->findOrFail($shipmentId);
// Use the shipment items to calculate totals
$shipment->total_ctn = $shipment->items->sum('order_ctn');
$shipment->total_qty = $shipment->items->sum('order_qty');
$shipment->total_ttl_qty = $shipment->items->sum('order_ttl_qty');
$shipment->total_amount = $shipment->items->sum('order_ttl_amount');
$shipment->total_cbm = $shipment->items->sum('order_cbm');
$shipment->total_ttl_cbm = $shipment->items->sum('order_ttl_cbm');
$shipment->total_kg = $shipment->items->sum('order_kg');
$shipment->total_ttl_kg = $shipment->items->sum('order_ttl_kg');
$shipment->save();
$shipment->refresh(); // Refresh to get updated data
return $shipment;
}
// Helper method to get available orders for a shipment
public function getAvailableOrders($shipmentId)
{
$shipment = Shipment::findOrFail($shipmentId);
// Get all used order IDs
$usedOrderIds = ShipmentItem::pluck('order_id')->toArray();
// Remove orders that are already in this shipment
$shipmentOrderIds = $shipment->items->pluck('order_id')->toArray();
$availableOrderIds = array_diff($usedOrderIds, $shipmentOrderIds);
$availableOrders = Order::whereNotIn('id', $availableOrderIds)->get();
return response()->json([
'success' => true,
'availableOrders' => $availableOrders
]);
}
}

View File

@@ -1,22 +1,43 @@
<?php
// app/Models/Admin.php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\Permission\Traits\HasRoles;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Facades\Hash;
class Admin extends Authenticatable
{
use Notifiable;
use HasFactory, Notifiable, HasRoles;
protected $guard = 'admin';
protected $guard_name = 'admin';
protected $fillable = [
'name', 'email', 'password', 'role',
'name', 'email', 'password', 'username',
'phone', 'emergency_phone', 'address',
'role', 'department', 'designation', 'joining_date',
'status', 'additional_info', 'type', // admin/staff indicator
];
protected $hidden = [
'password', 'remember_token',
];
public function setPasswordAttribute($value)
{
if (!$value) return;
if (Hash::needsRehash($value)) {
$this->attributes['password'] = Hash::make($value);
} else {
$this->attributes['password'] = $value;
}
}
public function getDisplayNameAttribute()
{
return "{$this->name}";
}
}

81
app/Models/Staff.php Normal file
View File

@@ -0,0 +1,81 @@
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Permission\Traits\HasRoles;
use Illuminate\Support\Facades\Hash;
class Staff extends Authenticatable
{
use Notifiable, HasRoles, SoftDeletes;
/**
* The guard name used by Spatie.
* Make sure this matches the guard you'll use for admin/staff auth (usually 'web' or 'admin').
*/
protected $guard_name = 'admin';
/**
* The attributes that are mass assignable.
*/
protected $fillable = [
'employee_id',
'name',
'email',
'phone',
'emergency_phone',
'address',
'role', // business role/title (not Spatie role)
'department',
'designation',
'joining_date',
'status',
'additional_info',
'username',
'password',
];
/**
* Hidden attributes (not returned in arrays / JSON).
*/
protected $hidden = [
'password',
];
/**
* Casts
*/
protected $casts = [
'joining_date' => '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 . ')';
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array<class-string, class-string>
*/
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;
}
});
}
}

View File

@@ -3,4 +3,5 @@
return [
App\Providers\AppServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\AuthServiceProvider::class,
];

View File

@@ -10,9 +10,10 @@
"barryvdh/laravel-dompdf": "^3.1",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"maatwebsite/excel": "^3.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",

639
composer.lock generated
View File

@@ -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": "c33d415efbeb5f4bc492cea187970455",
"content-hash": "0ed807863dbf93f06565547ce4303f47",
"packages": [
{
"name": "barryvdh/laravel-dompdf",
@@ -212,162 +212,6 @@
],
"time": "2024-02-09T16:56:22+00:00"
},
{
"name": "composer/pcre",
"version": "3.3.2",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<1.11.10"
},
"require-dev": {
"phpstan/phpstan": "^1.12 || ^2",
"phpstan/phpstan-strict-rules": "^1 || ^2",
"phpunit/phpunit": "^8 || ^9"
},
"type": "library",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Pcre\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
"keywords": [
"PCRE",
"preg",
"regex",
"regular expression"
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.3.2"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-11-12T16:29:46+00:00"
},
{
"name": "composer/semver",
"version": "3.4.4",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.11",
"symfony/phpunit-bridge": "^3 || ^7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Semver\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nils Adermann",
"email": "naderman@naderman.de",
"homepage": "http://www.naderman.de"
},
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
},
{
"name": "Rob Bast",
"email": "rob.bast@gmail.com",
"homepage": "http://robbast.nl"
}
],
"description": "Semver library that offers utilities, version constraint parsing and validation.",
"keywords": [
"semantic",
"semver",
"validation",
"versioning"
],
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/3.4.4"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
}
],
"time": "2025-08-20T19:15:30+00:00"
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.3",
@@ -896,67 +740,6 @@
],
"time": "2025-03-06T22:45:56+00:00"
},
{
"name": "ezyang/htmlpurifier",
"version": "v4.19.0",
"source": {
"type": "git",
"url": "https://github.com/ezyang/htmlpurifier.git",
"reference": "b287d2a16aceffbf6e0295559b39662612b77fcf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf",
"reference": "b287d2a16aceffbf6e0295559b39662612b77fcf",
"shasum": ""
},
"require": {
"php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
},
"require-dev": {
"cerdic/css-tidy": "^1.7 || ^2.0",
"simpletest/simpletest": "dev-master"
},
"suggest": {
"cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
"ext-bcmath": "Used for unit conversion and imagecrash protection",
"ext-iconv": "Converts text to and from non-UTF-8 encodings",
"ext-tidy": "Used for pretty-printing HTML"
},
"type": "library",
"autoload": {
"files": [
"library/HTMLPurifier.composer.php"
],
"psr-0": {
"HTMLPurifier": "library/"
},
"exclude-from-classmap": [
"/library/HTMLPurifier/Language/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "Edward Z. Yang",
"email": "admin@htmlpurifier.org",
"homepage": "http://ezyang.com"
}
],
"description": "Standards compliant HTML filter written in PHP",
"homepage": "http://htmlpurifier.org/",
"keywords": [
"html"
],
"support": {
"issues": "https://github.com/ezyang/htmlpurifier/issues",
"source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0"
},
"time": "2025-10-17T16:34:55+00:00"
},
{
"name": "fruitcake/php-cors",
"version": "v1.4.0",
@@ -2540,58 +2323,48 @@
},
{
"name": "maatwebsite/excel",
"version": "3.1.67",
"version": "v1.1.5",
"source": {
"type": "git",
"url": "https://github.com/SpartnerNL/Laravel-Excel.git",
"reference": "e508e34a502a3acc3329b464dad257378a7edb4d"
"url": "https://github.com/Maatwebsite/Laravel-Excel.git",
"reference": "0c67aba8387726458d42461eae91a3415593bbc4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/e508e34a502a3acc3329b464dad257378a7edb4d",
"reference": "e508e34a502a3acc3329b464dad257378a7edb4d",
"url": "https://api.github.com/repos/Maatwebsite/Laravel-Excel/zipball/0c67aba8387726458d42461eae91a3415593bbc4",
"reference": "0c67aba8387726458d42461eae91a3415593bbc4",
"shasum": ""
},
"require": {
"composer/semver": "^3.3",
"ext-json": "*",
"illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0||^12.0",
"php": "^7.0||^8.0",
"phpoffice/phpspreadsheet": "^1.30.0",
"psr/simple-cache": "^1.0||^2.0||^3.0"
"php": ">=5.3.0",
"phpoffice/phpexcel": "~1.8.0"
},
"require-dev": {
"laravel/scout": "^7.0||^8.0||^9.0||^10.0",
"orchestra/testbench": "^6.0||^7.0||^8.0||^9.0||^10.0",
"predis/predis": "^1.1"
"mockery/mockery": "~0.9",
"orchestra/testbench": "~2.2.0@dev",
"phpunit/phpunit": "~4.0"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Excel": "Maatwebsite\\Excel\\Facades\\Excel"
},
"providers": [
"Maatwebsite\\Excel\\ExcelServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"psr-0": {
"Maatwebsite\\Excel\\": "src/"
}
},
"classmap": [
"src/Maatwebsite/Excel",
"tests/TestCase.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
"LGPL"
],
"authors": [
{
"name": "Patrick Brouwers",
"email": "patrick@spartner.nl"
"name": "Maatwebsite.nl",
"email": "patrick@maatwebsite.nl"
}
],
"description": "Supercharged Excel exports and imports in Laravel",
"description": "An eloquent way of importing and exporting Excel and CSV in Laravel 4 with the power of PHPExcel",
"keywords": [
"PHPExcel",
"batch",
@@ -2599,210 +2372,13 @@
"excel",
"export",
"import",
"laravel",
"php",
"phpspreadsheet"
"laravel"
],
"support": {
"issues": "https://github.com/SpartnerNL/Laravel-Excel/issues",
"source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.67"
"issues": "https://github.com/Maatwebsite/Laravel-Excel/issues",
"source": "https://github.com/Maatwebsite/Laravel-Excel/tree/master"
},
"funding": [
{
"url": "https://laravel-excel.com/commercial-support",
"type": "custom"
},
{
"url": "https://github.com/patrickbrouwers",
"type": "github"
}
],
"time": "2025-08-26T09:13:16+00:00"
},
{
"name": "maennchen/zipstream-php",
"version": "3.1.2",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f",
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"ext-zlib": "*",
"php-64bit": "^8.2"
},
"require-dev": {
"brianium/paratest": "^7.7",
"ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.16",
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
"phpunit/phpunit": "^11.0",
"vimeo/psalm": "^6.0"
},
"suggest": {
"guzzlehttp/psr7": "^2.4",
"psr/http-message": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"ZipStream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paul Duncan",
"email": "pabs@pablotron.org"
},
{
"name": "Jonatan Männchen",
"email": "jonatan@maennchen.ch"
},
{
"name": "Jesse Donat",
"email": "donatj@gmail.com"
},
{
"name": "András Kolesár",
"email": "kolesar@kolesar.hu"
}
],
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
"keywords": [
"stream",
"zip"
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2"
},
"funding": [
{
"url": "https://github.com/maennchen",
"type": "github"
}
],
"time": "2025-01-27T12:07:53+00:00"
},
{
"name": "markbaker/complex",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPComplex.git",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Complex\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@lange.demon.co.uk"
}
],
"description": "PHP Class for working with complex numbers",
"homepage": "https://github.com/MarkBaker/PHPComplex",
"keywords": [
"complex",
"mathematics"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
},
"time": "2022-12-06T16:21:08+00:00"
},
{
"name": "markbaker/matrix",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPMatrix.git",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpdocumentor/phpdocumentor": "2.*",
"phploc/phploc": "^4.0",
"phpmd/phpmd": "2.*",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"sebastian/phpcpd": "^4.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Matrix\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@demon-angel.eu"
}
],
"description": "PHP Class for working with matrices",
"homepage": "https://github.com/MarkBaker/PHPMatrix",
"keywords": [
"mathematics",
"matrix",
"vector"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
},
"time": "2022-12-02T22:17:43+00:00"
"time": "2014-07-10T09:06:07+00:00"
},
{
"name": "masterminds/html5",
@@ -3825,110 +3401,66 @@
"time": "2025-02-10T21:11:16+00:00"
},
{
"name": "phpoffice/phpspreadsheet",
"version": "1.30.1",
"name": "phpoffice/phpexcel",
"version": "1.8.1",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "fa8257a579ec623473eabfe49731de5967306c4c"
"url": "https://github.com/PHPOffice/PHPExcel.git",
"reference": "372c7cbb695a6f6f1e62649381aeaa37e7e70b32"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fa8257a579ec623473eabfe49731de5967306c4c",
"reference": "fa8257a579ec623473eabfe49731de5967306c4c",
"url": "https://api.github.com/repos/PHPOffice/PHPExcel/zipball/372c7cbb695a6f6f1e62649381aeaa37e7e70b32",
"reference": "372c7cbb695a6f6f1e62649381aeaa37e7e70b32",
"shasum": ""
},
"require": {
"composer/pcre": "^1||^2||^3",
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zip": "*",
"ext-zlib": "*",
"ezyang/htmlpurifier": "^4.15",
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"php": ">=7.4.0 <8.5.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^1.0 || ^2.0 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.3",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^8.5 || ^9.0",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},
"suggest": {
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
"ext-intl": "PHP Internationalization Functions",
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
"php": ">=5.2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
"psr-0": {
"PHPExcel": "Classes/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
"LGPL"
],
"authors": [
{
"name": "Maarten Balliauw",
"homepage": "https://blog.maartenballiauw.be"
"homepage": "http://blog.maartenballiauw.be"
},
{
"name": "Mark Baker",
"homepage": "https://markbakeruk.net"
"name": "Mark Baker"
},
{
"name": "Franck Lefevre",
"homepage": "https://rootslabs.net"
"homepage": "http://blog.rootslabs.net"
},
{
"name": "Erik Tilt"
},
{
"name": "Adrien Crivelli"
}
],
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
"description": "PHPExcel - OpenXML - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
"homepage": "http://phpexcel.codeplex.com",
"keywords": [
"OpenXML",
"excel",
"gnumeric",
"ods",
"php",
"spreadsheet",
"xls",
"xlsx"
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.1"
"issues": "https://github.com/PHPOffice/PHPExcel/issues",
"source": "https://github.com/PHPOffice/PHPExcel/tree/master"
},
"time": "2025-10-26T16:01:04+00:00"
"abandoned": "phpoffice/phpspreadsheet",
"time": "2015-05-01T07:00:55+00:00"
},
{
"name": "phpoption/phpoption",
@@ -4832,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",

View File

@@ -85,6 +85,11 @@ return [
// 'driver' => 'database',
// 'table' => 'users',
// ],
'staff' => [
'driver' => 'eloquent',
'model' => App\Models\Staff::class,
],
],
/*

202
config/permission.php Normal file
View File

@@ -0,0 +1,202 @@
<?php
return [
'models' => [
/*
* 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',
],
];

View File

@@ -0,0 +1,134 @@
<?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
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), Exception::class, 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
// $table->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']);
}
};

View File

@@ -0,0 +1,50 @@
<?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()
{
Schema::create('staff', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -0,0 +1,41 @@
<?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::table('admins', function (Blueprint $table) {
$table->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) {
//
});
}
};

View File

@@ -0,0 +1,20 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up()
{
Schema::table('admins', function (Blueprint $table) {
$table->string('role')->nullable()->change(); // <-- Fix problem
});
}
public function down()
{
Schema::table('admins', function (Blueprint $table) {
$table->enum('role', ['admin', 'super-admin'])->nullable()->change();
});
}
};

View File

@@ -0,0 +1,32 @@
<?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('support_tickets', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id'); // user who owns the chat
$table->string('status')->default('open'); // open / closed
$table->timestamps();
// foreign key constraint (optional but recommended)
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('support_tickets');
}
};

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('chat_messages', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('ticket_id'); // support ticket ID
$table->unsignedBigInteger('sender_id'); // user or admin/staff
$table->text('message')->nullable(); // message content
$table->string('file_path')->nullable(); // image/pdf/video
$table->string('file_type')->default('text'); // text/image/pdf/video
$table->timestamps();
// foreign keys
$table->foreign('ticket_id')->references('id')->on('support_tickets')->onDelete('cascade');
$table->foreign('sender_id')->references('id')->on('users')->onDelete('cascade'); // admin also stored in users table? If admin separate, change later.
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('chat_messages');
}
};

View File

@@ -0,0 +1,103 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class PermissionSeeder extends Seeder
{
public function run()
{
// ------------------------------------------------------
// FINAL PERMISSION LIST (YOUR DATA)
// ------------------------------------------------------
$permissions = [
// ORDER
'order.view',
'order.create',
'order.edit',
'order.delete',
// EXTRA (ORDERS)
'orders.view', // you added this separately
// SHIPMENT
'shipment.view',
'shipment.create',
'shipment.delete',
// INVOICE
'invoice.view',
'invoice.edit',
'invoice.add_installment',
// CUSTOMER
'customer.view',
'customer.create',
// REQUEST
'request.view',
'request.update_profile',
// @can('')
// @endcan
// ACCOUNT
'account.view',
'account.create_order',
'account.edit_order',
'account.delete_order',
'account.toggle_payment_status',
'account.add_installment',
'account.view_installments',
// REPORT
'report.view',
// MARK LIST
'mark_list.view',
];
// ------------------------------------------------------
// CREATE PERMISSIONS
// ------------------------------------------------------
foreach ($permissions as $permission) {
Permission::firstOrCreate(
['name' => $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);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -809,9 +809,11 @@
All
</a>
@can('customer.create')
<a href="{{ route('admin.customers.add') }}" class="add-customer-btn">
<i class="bi bi-plus-circle me-1"></i>Add Customer
</a>
@endcan
</div>
</div>
</div>

View File

@@ -1131,9 +1131,12 @@ body, .container-fluid {
<div class="order-mgmt-box">
<div class="order-mgmt-bar">
<span class="order-mgmt-title"><i class="bi bi-table"></i> Order Management</span>
<button class="create-order-btn" id="openCreateOrderModal">
<i class="bi bi-plus-circle"></i> Create Order
</button>
@can('order.create')
<button class="create-order-btn" id="openCreateOrderModal">
<i class="bi bi-plus-circle"></i> Create Order
</button>
@endcan
</div>
<div class="order-mgmt-main">

View File

@@ -452,9 +452,11 @@ body {
</div>
<div class="text-end mt-3">
@can('invoice.edit')
<button type="submit" class="btn-success-compact btn-compact">
<i class="fas fa-save me-2"></i>Update Invoice
</button>
@endcan
</div>
</form>
</div>
@@ -541,11 +543,13 @@ body {
<h4>
<i class="fas fa-credit-card me-2"></i>Installment Payments
</h4>
@if($remaining > 0)
<button id="toggleInstallmentForm" class="btn-primary-compact btn-compact">
<i class="fas fa-plus-circle me-2"></i>Add Installment
</button>
@endif
@can('invoice.add_installment')
@if($remaining > 0)
<button id="toggleInstallmentForm" class="btn-primary-compact btn-compact">
<i class="fas fa-plus-circle me-2"></i>Add Installment
</button>
@endif
@endcan
</div>
<div class="card-body-compact">

View File

@@ -17,6 +17,7 @@
font-family: 'Inter', Arial, sans-serif;
display: flex;
flex-direction: row;
transition: all 0.3s ease-in-out;
}
/* ✨ Sidebar Glass + Animated Highlight Effect */
@@ -36,7 +37,13 @@
position: fixed;
top: 0;
left: 0;
}
/* Sidebar collapsed state */
.sidebar.collapsed {
transform: translateX(-100%);
opacity: 0;
visibility: hidden;
}
.sidebar .logo {
@@ -73,7 +80,6 @@
overflow: hidden;
transition: all 0.25s ease;
z-index: 0;
}
/* Background Animation */
@@ -151,6 +157,39 @@
flex-direction: column;
width: calc(100vw - 190px);
margin-left: 190px;
transition: all 0.3s ease-in-out;
}
/* Main content when sidebar is collapsed */
.main-content.expanded {
margin-left: 0;
width: 100vw;
}
/* Header hamburger button */
.header-toggle {
background: transparent;
border: none;
cursor: pointer;
padding: 8px;
margin-right: 15px;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s ease;
width: 40px;
height: 40px;
border-radius: 8px;
}
.header-toggle:hover {
transform: scale(1.1);
background: rgba(43, 92, 182, 0.1);
}
.header-toggle i {
font-size: 1.6rem;
color: #2b5cb6;
}
header {
@@ -165,6 +204,11 @@
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.header-left {
display: flex;
align-items: center;
}
.content-wrapper {
padding: 18px 16px 16px 16px;
flex-grow: 1;
@@ -184,54 +228,96 @@
<div class="word"><strong>KENT</strong><br /><small>International Pvt. Ltd.</small></div>
</div>
<a href="{{ route('admin.dashboard') }}" class="{{ request()->routeIs('admin.dashboard') ? 'active' : '' }}"><i class="bi bi-house"></i> Dashboard</a>
<a href="{{ route('admin.shipments') }}" class="{{ request()->routeIs('admin.shipments') ? 'active' : '' }}"><i class="bi bi-truck"></i> Shipments</a>
<a href="{{ route('admin.invoices.index') }}"
class="{{ request()->routeIs('admin.invoices.index') ? 'active' : '' }}">
{{-- Dashboard (requires order.view) --}}
@can('order.view')
<a href="{{ route('admin.dashboard') }}" class="{{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
<i class="bi bi-house"></i> Dashboard
</a>
@endcan
{{-- Shipments --}}
@can('shipment.view')
<a href="{{ route('admin.shipments') }}" class="{{ request()->routeIs('admin.shipments') ? 'active' : '' }}">
<i class="bi bi-truck"></i> Shipments
</a>
@endcan
{{-- Invoice --}}
@can('invoice.view')
<a href="{{ route('admin.invoices.index') }}" class="{{ request()->routeIs('admin.invoices.index') ? 'active' : '' }}">
<i class="bi bi-receipt"></i> Invoice
</a>
@endcan
<a href="{{ route('admin.customers.index') }}"
class="{{ request()->routeIs('admin.customers.index') ? 'active' : '' }}">
{{-- Customers --}}
@can('customer.view')
<a href="{{ route('admin.customers.index') }}" class="{{ request()->routeIs('admin.customers.index') ? 'active' : '' }}">
<i class="bi bi-people"></i> Customers
</a>
@endcan
<a href="{{ route('admin.reports') }}"
class="{{ request()->routeIs('admin.reports') ? 'active' : '' }}">
{{-- Reports --}}
@can('report.view')
<a href="{{ route('admin.reports') }}" class="{{ request()->routeIs('admin.reports') ? 'active' : '' }}">
<i class="bi bi-graph-up"></i> Reports
</a>
@endcan
<a href="{{ route('admin.chat_support') }}" class="{{ request()->routeIs('admin.chat_support') ? 'active' : '' }}"><i class="bi bi-chat-dots"></i> Chat Support</a>
<!-- <a href="{{ route('admin.orders.index') }}"
class="{{ request()->routeIs('admin.orders.*') ? 'active' : '' }}">
<i class="bi bi-bag"></i> Orders
</a> -->
<a href="{{ route('admin.orders') }}"
class="{{ request()->routeIs('admin.orders') ? 'active' : '' }}">
<i class="bi bi-bag"></i> Orders
</a>
<a href="{{ route('admin.requests') }}" class="{{ request()->routeIs('admin.requests') ? 'active' : '' }}"><i class="bi bi-envelope"></i> Requests</a>
<li>
<a href="{{ route('admin.profile.requests') }}">
<i class="bi bi-person-lines-fill"></i>
Profile Update Requests
{{-- Chat Support (NO PERMISSION REQUIRED) --}}
<a href="{{ route('admin.chat_support') }}" class="{{ request()->routeIs('admin.chat_support') ? 'active' : '' }}">
<i class="bi bi-chat-dots"></i> Chat Support
</a>
</li>
<a href="{{ route('admin.staff') }}" class="{{ request()->routeIs('admin.staff') ? 'active' : '' }}"><i class="bi bi-person-badge"></i> Staff</a>
<a href="{{ route('admin.account') }}" class="{{ request()->routeIs('admin.account') ? 'active' : '' }}"><i class="bi bi-gear"></i> Account</a>
<a href="{{ route('admin.marklist.index') }}" class="{{ request()->routeIs('admin.marklist.index') ? 'active' : '' }}"><i class="bi bi-list-check"></i> Mark List</a>
{{-- Orders --}}
@can('orders.view')
<a href="{{ route('admin.orders') }}" class="{{ request()->routeIs('admin.orders') ? 'active' : '' }}">
<i class="bi bi-bag"></i> Orders
</a>
@endcan
<!-- <form method="POST" action="{{ route('admin.logout') }}" class="mt-4 px-3">
@csrf
<button type="submit" class="btn btn-danger w-100"><i class="bi bi-box-arrow-right"></i> Logout</button>
</form> -->
{{-- Requests --}}
@can('request.view')
<a href="{{ route('admin.requests') }}" class="{{ request()->routeIs('admin.requests') ? 'active' : '' }}">
<i class="bi bi-envelope"></i> Requests
</a>
@endcan
{{-- Profile Update Requests --}}
@can('request.update_profile')
<a href="{{ route('admin.profile.requests') }}">
<i class="bi bi-person-lines-fill"></i> Profile Update Requests
</a>
@endcan
{{-- Staff (NO PERMISSION REQUIRED) --}}
<a href="{{ route('admin.staff.index') }}" class="{{ request()->routeIs('admin.staff.*') ? 'active' : '' }}">
<i class="bi bi-person-badge"></i> Staff
</a>
{{-- Account Section --}}
@can('account.view')
<a href="{{ route('admin.account') }}" class="{{ request()->routeIs('admin.account') ? 'active' : '' }}">
<i class="bi bi-gear"></i> Account
</a>
@endcan
{{-- Mark List --}}
@can('mark_list.view')
<a href="{{ route('admin.marklist.index') }}" class="{{ request()->routeIs('admin.marklist.index') ? 'active' : '' }}">
<i class="bi bi-list-check"></i> Mark List
</a>
@endcan
</div>
<div class="main-content">
<header>
<h5>@yield('page-title')</h5>
<div class="header-left">
<button class="header-toggle" id="headerToggle">
<i class="bi bi-list"></i>
</button>
<h5 class="mb-0">@yield('page-title')</h5>
</div>
<div class="d-flex align-items-center gap-3">
<i class="bi bi-bell position-relative fs-4">
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">2</span>
@@ -260,5 +346,23 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const headerToggle = document.getElementById('headerToggle');
const sidebar = document.querySelector('.sidebar');
const mainContent = document.querySelector('.main-content');
// Function to toggle sidebar
function toggleSidebar() {
sidebar.classList.toggle('collapsed');
mainContent.classList.toggle('expanded');
}
// Header toggle button click event
if (headerToggle) {
headerToggle.addEventListener('click', toggleSidebar);
}
});
</script>
</body>
</html>
</html>

View File

@@ -120,19 +120,33 @@
@endif
<form method="POST" action="{{ route('admin.login.submit') }}">
@csrf
<div class="mb-3">
<label>Email</label>
<input type="email" name="email" class="form-control" value="{{ old('email') }}" required>
</div>
@csrf
<div class="mb-3">
<label>Password</label>
<input type="password" name="password" class="form-control" required>
</div>
<div class="mb-3">
<label>Username / Email / Employee ID</label>
<input
type="text"
name="login"
class="form-control"
value="{{ old('login') }}"
placeholder="Enter Email or Username or EMP ID"
required
>
</div>
<div class="mb-3">
<label>Password</label>
<input
type="password"
name="password"
class="form-control"
required
>
</div>
<button type="submit" class="btn btn-primary w-100">Login</button>
</form>
<button type="submit" class="btn btn-primary w-100">Login</button>
</form>
<div class="secure-encrypted">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="green" class="bi bi-lock-fill" viewBox="0 0 16 16">

View File

@@ -748,10 +748,13 @@
<script>
// Pagination state
let currentPage = 1;
const itemsPerPage = 10;
let allOrders = @json($orders);
let filteredOrders = [...allOrders];
let currentPage = 1;
const itemsPerPage = 10;
let allOrders = @json($orders);
let filteredOrders = [...allOrders];
console.log('ORDERS SAMPLE:', allOrders[0]);
// Status icon helper functions
function getInvoiceStatusIcon(status) {
@@ -931,10 +934,11 @@
const shipmentFilter = document.getElementById('shipmentFilter').value;
filteredOrders = allOrders.filter(order => {
const matchesSearch = !searchTerm ||
(order.order_id && order.order_id.toLowerCase().includes(searchTerm)) ||
(order.markList?.company_name && order.markList.company_name.toLowerCase().includes(searchTerm)) ||
(order.invoice?.invoice_number && order.invoice.invoice_number.toLowerCase().includes(searchTerm));
const matchesSearch = !searchTerm ||
order.order_id?.toLowerCase().includes(searchTerm) ||
order.mark_list?.company_name?.toLowerCase().includes(searchTerm) ||
order.invoice?.invoice_number?.toLowerCase().includes(searchTerm);
const matchesStatus = !statusFilter ||
(order.invoice?.status && order.invoice.status.toLowerCase() === statusFilter);
@@ -1049,7 +1053,7 @@
const paginatedItems = filteredOrders.slice(startIndex, endIndex);
paginatedItems.forEach(order => {
const mark = order.markList || null;
const mark = order.mark_list || null;
const invoice = order.invoice || null;
const shipment = order.shipments?.[0] || null;
const invoiceStatus = (invoice?.status || '').toLowerCase();

View File

@@ -17,9 +17,11 @@
</div>
{{-- ADD ITEM --}}
@can('order.create')
<button class="btn btn-add-item" data-bs-toggle="modal" data-bs-target="#addItemModal">
<i class="fas fa-plus-circle me-2"></i>Add New Item
</button>
@endcan
<a href="{{ route('admin.dashboard') }}" class="btn-close"></a>
</div>
@@ -191,6 +193,7 @@
<td class="d-flex justify-content-center gap-2">
{{-- EDIT BUTTON --}}
@can('order.edit')
<button
type="button"
class="btn btn-sm btn-edit-item"
@@ -198,7 +201,9 @@
data-bs-target="#editItemModal{{ $item->id }}">
<i class="fas fa-edit"></i>
</button>
@endcan
@can('order.delete')
{{-- DELETE BUTTON --}}
<form action="{{ route('admin.orders.deleteItem', $item->id) }}"
method="POST"
@@ -209,6 +214,7 @@
<i class="fas fa-trash"></i>
</button>
</form>
@endcan
</td>
</tr>
@@ -327,7 +333,7 @@
<div class="modal-footer">
<div class="modal-footer">
<button type="button" class="btn btn-modal-close" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-modal-add">Add Item</button>
<button type="submit" class="btn btn-modal-add">Edit Item</button>
</div>
</div>

View File

@@ -63,7 +63,6 @@
--hover-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
/* UPDATED: Search Bar Styles - White Background */
.search-shipment-bar {
display: flex;
align-items: center;
@@ -90,69 +89,9 @@
z-index: 0;
}
.search-input-container:focus-within {
border-color: #4361ee;
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.1);
}
.search-shipment-bar input {
padding: 12px 16px;
border: none;
flex: 1;
background: transparent;
font-weight: 500;
transition: all 0.3s ease;
font-family: 'Inter', sans-serif;
font-size: 14px;
outline: none;
color: #333;
}
.search-shipment-bar input::placeholder {
color: #6b7280;
}
/* UPDATED: Search Button - White with Blue Icon by default, Gradient on hover */
.search-button {
background: white;
color: #4361ee;
border: none;
padding: 12px 20px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 600;
font-family: 'Inter', sans-serif;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
border-radius: 0 10px 10px 0;
min-width: 100px;
justify-content: center;
border-left: 1px solid #d1d5db;
}
.search-input-group {
border: 1px solid #d1d5db !important;
border-radius: 8px !important;
padding: 4px !important;
background: #ffffff !important;
}
.search-button:hover {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.search-icon {
font-size: 16px;
color: #4361ee;
transition: all 0.3s ease;
}
.search-button:hover .search-icon {
color: white;
.search-shipment-bar > * {
position: relative;
z-index: 1;
}
.search-shipment-bar input,
@@ -215,27 +154,7 @@
width: 100%;
}
}
/* VIEW BUTTON STYLING */
.btn-view {
background: linear-gradient(135deg, #4cc9f0, #4361ee) !important;
border: none !important;
border-radius: 10px !important;
padding: 8px 14px !important;
color: white !important;
box-shadow: 0 4px 12px rgba(67, 97, 238, 0.35) !important;
transition: all 0.3s ease !important;
}
.btn-view:hover {
transform: translateY(-2px) scale(1.05) !important;
background: linear-gradient(135deg, #3a56d4, #4cc9f0) !important;
box-shadow: 0 8px 20px rgba(67, 97, 238, 0.5) !important;
}
.btn-view i {
font-size: 16px !important;
}
/* Card Styles */
.card {
border: none;
@@ -325,8 +244,8 @@
.table tbody tr:last-child td {
border-bottom: none;
}
/* UPDATED: Status Badge Styles - ALL SAME SIZE WITH ICONS */
/* UPDATED: Status Badge Styles - ALL SAME SIZE */
.badge {
padding: 7px 17px !important;
border-radius: 20px !important;
@@ -339,15 +258,15 @@
line-height: 1.2 !important;
}
/* Status icons */
.status-icon {
font-size: 13px;
display: flex;
align-items: center;
justify-content: center;
/* Loading Status - PROPER */
.badge-loading {
background: linear-gradient(135deg, #e3f2fd, #90caf9) !important;
color: #1565c0 !important;
border-color: #2196f3 !important;
width: 110px;
}
/* Pending Status - SAME SIZE WITH CLOCK ICON */
/* Pending Status - SAME SIZE */
.badge-pending {
background: linear-gradient(135deg, #fef3c7, #fde68a) !important;
color: #d97706 !important;
@@ -355,7 +274,7 @@
width: 110px;
}
/* In Transit Status - SAME SIZE WITH TRUCK ICON */
/* In Transit Status - SAME SIZE */
.badge-in_transit {
background: linear-gradient(135deg, #dbeafe, #93c5fd) !important;
color: #1e40af !important;
@@ -363,7 +282,7 @@
width: 110px;
}
/* Dispatched Status - SAME SIZE WITH BOX ICON */
/* Dispatched Status - SAME SIZE */
.badge-dispatched {
background: linear-gradient(135deg, #e9d5ff, #c4b5fd) !important;
color: #6b21a8 !important;
@@ -371,7 +290,7 @@
width: 110px;
}
/* Delivered Status - SAME SIZE WITH CHECK ICON */
/* Delivered Status - SAME SIZE */
.badge-delivered {
background: linear-gradient(135deg, #d1fae5, #a7f3d0) !important;
color: #065f46 !important;
@@ -409,7 +328,30 @@
padding: 6px 12px !important;
}
/* NEW: Action Button Styles */
/* Eye Button Style - PROPER */
.btn-eye {
background: linear-gradient(135deg, #4cc9f0, #4361ee);
color: white;
border: none;
border-radius: 8px;
padding: 8px 10px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
margin: 0 auto;
}
.btn-eye:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(76, 201, 240, 0.3);
background: linear-gradient(135deg, #38bdf8, #3a56d4);
}
/* Action Button Styles */
.action-container {
position: relative;
display: inline-block;
@@ -457,6 +399,16 @@
display: flex;
}
/* Loading Status Option - PROPER */
.status-option.loading {
background: rgba(33, 150, 243, 0.1);
color: #2196f3;
}
.status-option.loading:hover {
background: rgba(33, 150, 243, 0.2);
}
.status-option {
padding: 10px 12px;
border: none;
@@ -519,6 +471,11 @@
border-radius: 50%;
display: inline-block;
}
/* Loading Status Indicator - PROPER */
.status-indicator.loading {
background: #2196f3;
}
.status-indicator.pending {
background: #f8961e;
@@ -536,66 +493,6 @@
background: #10b981;
}
/* NEW: View Button Styles - Icon Only */
.btn-view {
background: #4361ee;
color: white;
border: none;
border-radius: 8px;
padding: 8px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
font-weight: 600;
font-size: 16px;
font-family: 'Inter', sans-serif;
position: relative;
overflow: hidden;
}
.btn-view::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
transition: all 0.3s ease;
z-index: 1;
}
.btn-view:hover::before {
left: 0;
}
.btn-view i {
position: relative;
z-index: 2;
transition: all 0.3s ease;
}
.btn-view:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-view:hover i {
transform: scale(1.1);
}
/* Action buttons container */
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
}
/* Modal Styles */
.modal-content {
border-radius: 20px;
@@ -830,9 +727,6 @@
transition: all 0.3s ease;
position: relative;
color: #4361ee !important;
font-family: 'Inter', sans-serif;
font-size: 14px;
cursor: pointer;
}
a.text-primary:hover {
@@ -840,7 +734,7 @@
text-decoration: underline;
}
/* Shipment Details Modal - UPDATED TO MATCH SECOND CODE */
/* Shipment Details Modal */
.shipment-details-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
@@ -930,7 +824,7 @@
border-bottom-right-radius: 10px;
}
/* Shipment Totals Section - UPDATED */
/* Shipment Totals Section */
.shipment-totals {
margin-top: 25px;
padding: 25px;
@@ -1214,48 +1108,8 @@
justify-content: center;
}
}
/* Edit Form Styles */
.edit-shipment-form {
background: #f8fafc;
padding: 25px;
border-radius: 12px;
margin-bottom: 20px;
border-left: 4px solid #4361ee;
}
.btn-save {
background: linear-gradient(135deg, #48bb78, #38a169);
color: white;
font-weight: 600;
border-radius: 8px;
padding: 10px 20px;
border: none;
transition: all 0.3s ease;
}
.btn-save:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(72, 187, 120, 0.3);
}
.btn-cancel-edit {
background: #f7fafc;
color: #718096;
border: 1px solid #cbd5e0;
border-radius: 8px;
font-weight: 600;
padding: 10px 20px;
transition: all 0.3s ease;
}
.btn-cancel-edit:hover {
background: #edf2f7;
color: #4a5568;
}
</style>
<div class="container-fluid py-4">
{{-- SUCCESS / ERROR MESSAGES --}}
@@ -1275,15 +1129,13 @@
</div>
@endif
<!-- ============================= -->
<!-- SEARCH BAR AND ADD BUTTON -->
<!-- ============================= -->
<div class="search-shipment-bar">
<span class="search-icon">🔍</span>
<input type="text" id="searchInput" placeholder="Search Shipments...">
<div class="status-filter-container">
<select id="statusFilter" class="status-filter-select">
<option value="all">All Status</option>
<option value="loading">Loading</option>
<option value="pending">Pending</option>
<option value="in_transit">In Transit</option>
<option value="dispatched">Dispatched</option>
@@ -1302,9 +1154,6 @@
</button>
</div>
<!-- ============================= -->
<!-- CREATE SHIPMENT MODAL -->
<!-- ============================= -->
<div class="modal fade" id="createShipmentModal" tabindex="-1" aria-labelledby="createShipmentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
@@ -1396,9 +1245,6 @@
</div>
</div>
<!-- ============================= -->
<!-- SHIPMENT LIST TABLE -->
<!-- ============================= -->
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-truck me-2"></i> Shipments List</h5>
@@ -1419,8 +1265,8 @@
<th>Total Amount</th>
<th>Status</th>
<th>Date</th>
<th>Action</th>
<th>View</th>
<th>Action</th>
</tr>
</thead>
@@ -1433,15 +1279,16 @@
{{-- REVERSE INDEX: सर्वात वरच्या shipment ला सर्वात मोठा क्रमांक --}}
<td class="fw-bold">{{ $totalShipments - $loop->index }}</td>
<td>
<a href="#" class="text-primary fw-bold" onclick="openShipmentDetails({{ $ship->id }})">
<a href="#" class="text-primary fw-bold"
onclick="openShipmentDetails({{ $ship->id }})">
{{ $ship->shipment_id }}
</a>
</td>
<td>{{ $ship->origin }}</td>
<td>{{ $ship->destination }}</td>
<td>{{ $ship->total_qty }}</td>
<td>{{ $ship->total_kg }} kg</td>
<td>{{ $ship->total_cbm }} CBM</td>
<td><span class="badge bg-light text-dark">{{ $ship->total_qty }}</span></td>
<td><span class="badge bg-light text-dark">{{ $ship->total_kg }} kg</span></td>
<td><span class="badge bg-light text-dark">{{ $ship->total_cbm }} CBM</span></td>
<td class="fw-bold text-success">{{ number_format($ship->total_amount, 2) }}</td>
<td>
<span class="badge badge-{{ $ship->status }}">
@@ -1449,6 +1296,12 @@
</span>
</td>
<td>{{ \Carbon\Carbon::parse($ship->shipment_date)->format('d M Y') }}</td>
<td>
<button type="button" class="btn-eye" onclick="openShipmentDetails({{ $ship->id }})" title="View Shipment">
<i class="bi bi-eye"></i>
</button>
</td>
<td>
<div class="action-container">
<button type="button" class="btn-edit-status" onclick="toggleStatusDropdown(this, {{ $ship->id }})" title="Edit Status">
@@ -1458,6 +1311,10 @@
<form action="{{ route('admin.shipments.updateStatus') }}" method="POST" class="status-form">
@csrf
<input type="hidden" name="shipment_id" value="{{ $ship->id }}">
<button type="submit" name="status" value="loading" class="status-option loading">
<span class="status-indicator loading"></span>
Loading
</button>
<button type="submit" name="status" value="pending" class="status-option pending">
<span class="status-indicator pending"></span>
Pending
@@ -1478,16 +1335,10 @@
</div>
</div>
</td>
<td>
<a href="{{ route('admin.shipments.view', ['id' => $ship->id, 'mode' => 'edit']) }}"
class="btn btn-view">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
@empty
<tr>
<td colspan="11" class="text-center py-5 text-muted">
<td colspan="12" class="text-center py-5 text-muted">
<i class="bi bi-inbox display-4 d-block mb-3"></i>
No shipments found
</td>
@@ -1497,21 +1348,17 @@
</table>
</div>
<!-- Pagination Controls -->
<div class="pagination-container">
<div class="pagination-info" id="pageInfo">Showing 1 to {{ $shipments->count() }} of {{ $shipments->count() }} entries</div>
<div class="pagination-controls">
<button class="pagination-img-btn" id="prevPageBtn" title="Previous page" disabled>
<!-- Left arrow SVG -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 12L6 8L10 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="pagination-pages" id="paginationPages">
<!-- Page numbers will be inserted here -->
</div>
</div>
<button class="pagination-img-btn" id="nextPageBtn" title="Next page" disabled>
<!-- Right arrow SVG -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 4L10 8L6 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1521,34 +1368,29 @@
</div>
</div>
</div>
{{-- SHIPMENT DETAILS MODAL --}}
<div class="modal fade" id="shipmentDetailsModal" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header shipment-details-header">
<h5 class="modal-title fw-bold"><i class="bi bi-box-seam me-2"></i>Consolidated Shipment Details</h5>
<button class="btn-close" data-bs-dismiss="modal"></button>
</div>
<!-- ============================= -->
<!-- SHIPMENT DETAILS MODAL -->
<!-- ============================= -->
<div class="modal fade" id="shipmentDetailsModal" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header shipment-details-header">
<h5 class="modal-title fw-bold"><i class="bi bi-box-seam me-2"></i>Consolidated Shipment Details</h5>
<button class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body shipment-details-body" id="shipmentDetailsContent">
<div class="text-center py-4 loading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
<div class="modal-body shipment-details-body" id="shipmentDetailsContent">
<div class="text-center py-4 loading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading shipment details...</p>
</div>
<p class="mt-2 text-muted">Loading shipment details...</p>
</div>
</div>
</div>
</div>
</div>
<!-- ========================= -->
<!-- MODAL LOAD SCRIPT (AJAX) -->
<!-- ========================= -->
<script>
// Pagination state
let currentPage = 1;
@@ -1701,7 +1543,7 @@ function renderTable() {
if (filteredShipments.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="11" class="text-center py-5 text-muted">
<td colspan="12" class="text-center py-5 text-muted">
<i class="bi bi-search display-4 d-block mb-3"></i>
No shipments found matching your criteria
</td>
@@ -1727,6 +1569,12 @@ function renderTable() {
row.setAttribute('data-status', shipment.status);
row.setAttribute('data-shipment-id', shipment.shipment_id);
// Function to format status string
const formatStatus = (status) => {
if (!status) return '';
return status.charAt(0).toUpperCase() + status.slice(1).replace(/_/g, ' ');
};
row.innerHTML = `
<td class="fw-bold">${displayIndex}</td>
<td>
@@ -1742,10 +1590,19 @@ function renderTable() {
<td class="fw-bold text-success">${parseFloat(shipment.total_amount).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</td>
<td>
<span class="badge badge-${shipment.status}">
${shipment.status.charAt(0).toUpperCase() + shipment.status.slice(1).replace('_', ' ')}
${formatStatus(shipment.status)}
</span>
</td>
<td>${new Date(shipment.shipment_date).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })}</td>
<td>
<a href="{{ route('admin.shipments.dummy', $ship->id) }}"
class="btn-view-details">
<i class="bi bi-eye"></i>
</a>
</td>
<td>
<div class="action-container">
<button type="button" class="btn-edit-status" onclick="toggleStatusDropdown(this, ${shipment.id})" title="Edit Status">
@@ -1755,6 +1612,10 @@ function renderTable() {
<form action="/admin/shipments/update-status" method="POST" class="status-form">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<input type="hidden" name="shipment_id" value="${shipment.id}">
<button type="submit" name="status" value="loading" class="status-option loading">
<span class="status-indicator loading"></span>
Loading
</button>
<button type="submit" name="status" value="pending" class="status-option pending">
<span class="status-indicator pending"></span>
Pending
@@ -1775,19 +1636,14 @@ function renderTable() {
</div>
</div>
</td>
<td>
<a href="/admin/shipments/view/${shipment.id}?mode=edit"
class="btn btn-view"
title="Edit Shipment">
<i class="bi bi-eye"></i>
</a>
</td>
`;
tbody.appendChild(row);
});
}
// Function to open shipment details modal
// --------------------------------------------------------------------
// Function: Open Consolidated Shipment Details Modal
// --------------------------------------------------------------------
function openShipmentDetails(id) {
let modal = new bootstrap.Modal(document.getElementById('shipmentDetailsModal'));
let content = document.getElementById('shipmentDetailsContent');
@@ -1815,8 +1671,13 @@ function openShipmentDetails(id) {
year: 'numeric'
});
// Function to format status string
const formatStatus = (status) => {
if (!status) return 'N/A';
return status.charAt(0).toUpperCase() + status.slice(1).replace(/_/g, ' ');
};
let html = `
<!-- Shipment Basic Info -->
<div class="shipment-info-row">
<div class="shipment-info-item">
<div class="shipment-info-label">Shipment ID</div>
@@ -1828,7 +1689,7 @@ function openShipmentDetails(id) {
</div>
<div class="shipment-info-item">
<div class="shipment-info-label">Status</div>
<div class="shipment-info-value">${data.shipment.status}</div>
<div class="shipment-info-value">${formatStatus(data.shipment.status)}</div>
</div>
<div class="shipment-info-item">
<div class="shipment-info-label">Date</div>
@@ -1863,7 +1724,9 @@ function openShipmentDetails(id) {
data.orders.forEach(order => {
html += `
<tr>
<td class="fw-bold text-primary">${order.order_id}</td>
<td class="fw-bold">
${order.order_id}
</td>
<td>${order.origin || 'N/A'}</td>
<td>${order.destination || 'N/A'}</td>
<td>${order.mark_no || 'ITEM001'}</td>
@@ -1885,7 +1748,6 @@ function openShipmentDetails(id) {
</table>
</div>
<!-- Totals Section -->
<div class="shipment-totals">
<div class="shipment-totals-row">
<div class="shipment-total-item">
@@ -1958,15 +1820,11 @@ function toggleStatusDropdown(button, shipmentId) {
// Close dropdown when clicking outside
document.addEventListener('click', function closeDropdown(e) {
// allow clicking links normally
if (e.target.closest('a')) return;
if (!button.contains(e.target) && !dropdown.contains(e.target)) {
dropdown.classList.remove('show');
document.removeEventListener('click', closeDropdown);
}
}, { once: true });
if (!button.contains(e.target) && !dropdown.contains(e.target)) {
dropdown.classList.remove('show');
document.removeEventListener('click', closeDropdown);
}
});
}
// Auto-close dropdown after form submission

View File

@@ -0,0 +1,187 @@
@extends('admin.layouts.app')
@section('page-title', 'Account Dashboard')
@section('content')
<style>
.card { background:#fff; border:1px solid #eaeaea; padding:1rem; border-radius:6px; }
.field { margin-bottom:.8rem; }
label { display:block; font-weight:600; margin-bottom:.25rem; }
input[type="text"], input[type="email"], input[type="date"], input[type="password"], textarea, select {
width:100%; padding:.5rem; border:1px solid #ddd; border-radius:4px;
}
.stages { display:flex; gap:.5rem; margin-bottom:1rem; }
.stage-ind { padding:.35rem .6rem; border-radius:4px; background:#f3f3f3; }
.stage { display:none; }
.stage.active { display:block; }
.btn { padding:.5rem .8rem; border-radius:5px; border:1px solid #ccc; background:#f6f6f6; cursor:pointer; }
.btn.primary { background:#0b74de; color:#fff; border-color:#0b74de; }
.perm-group { border:1px dashed #eee; padding:.6rem; margin-bottom:.6rem; border-radius:4px; }
.perm-list { display:flex; flex-wrap:wrap; gap:.5rem; }
.perm-item { min-width:200px; }
.error { color:#b00020; font-size:.95rem; margin-top:.25rem; }
</style>
<div class="card">
<h3>Add Staff</h3>
@if($errors->any())
<div style="background:#fff0f0; padding:.6rem; border:1px solid #f3c6c6; margin-bottom:1rem;">
<strong>There were some problems with your input:</strong>
<ul>
@foreach($errors->all() as $err)
<li>{{ $err }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('admin.staff.store') }}">
@csrf
<div class="stages">
<div class="stage-ind">1. Personal</div>
<div class="stage-ind">2. Professional</div>
<div class="stage-ind">3. System</div>
<div class="stage-ind">4. Permissions</div>
</div>
{{-- Stage 1 --}}
<div id="stage-1" class="stage active">
<div class="field">
<label>Name *</label>
<input type="text" name="name" value="{{ old('name') }}" required>
</div>
<div class="field">
<label>Email *</label>
<input type="email" name="email" value="{{ old('email') }}" required>
</div>
<div class="field">
<label>Phone *</label>
<input type="text" name="phone" value="{{ old('phone') }}" required>
</div>
<div class="field">
<label>Emergency Phone</label>
<input type="text" name="emergency_phone" value="{{ old('emergency_phone') }}">
</div>
<div class="field">
<label>Address</label>
<textarea name="address" rows="3">{{ old('address') }}</textarea>
</div>
<div style="display:flex; justify-content:flex-end; gap:.5rem;">
<button type="button" class="btn" onclick="showStage(2)">Next</button>
</div>
</div>
{{-- Stage 2 --}}
<div id="stage-2" class="stage">
<div class="field">
<label>Role (Business role)</label>
<input type="text" name="role" value="{{ old('role') }}">
</div>
<div class="field">
<label>Department</label>
<input type="text" name="department" value="{{ old('department') }}">
</div>
<div class="field">
<label>Designation</label>
<input type="text" name="designation" value="{{ old('designation') }}">
</div>
<div class="field">
<label>Joining Date</label>
<input type="date" name="joining_date" value="{{ old('joining_date') }}">
</div>
<div class="field">
<label>Status</label>
<select name="status">
<option value="active" {{ old('status')=='active'?'selected':'' }}>Active</option>
<option value="inactive" {{ old('status')=='inactive'?'selected':'' }}>Inactive</option>
</select>
</div>
<div class="field">
<label>Additional Info</label>
<textarea name="additional_info" rows="3">{{ old('additional_info') }}</textarea>
</div>
<div style="display:flex; justify-content:space-between; gap:.5rem;">
<button type="button" class="btn" onclick="showStage(1)">Back</button>
<button type="button" class="btn" onclick="showStage(3)">Next</button>
</div>
</div>
{{-- Stage 3 --}}
<div id="stage-3" class="stage">
<div class="field">
<label>Username *</label>
<input type="text" name="username" value="{{ old('username') }}" placeholder="leave blank to use EMPxxxx">
<div class="muted" style="font-size:.9rem; margin-top:.25rem;">If left blank employee id will be used.</div>
</div>
<div class="field">
<label>Password *</label>
<input type="password" name="password" required>
</div>
<div style="display:flex; justify-content:space-between; gap:.5rem;">
<button type="button" class="btn" onclick="showStage(2)">Back</button>
<button type="button" class="btn" onclick="showStage(4)">Next</button>
</div>
</div>
{{-- Stage 4 --}}
<div id="stage-4" class="stage">
<div style="margin-bottom:.5rem; font-weight:700;">Permissions</div>
@foreach($permissions as $group => $groupPerms)
<div class="perm-group">
<div style="display:flex; justify-content:space-between; align-items:center;">
<div style="font-weight:600;">{{ ucfirst($group) }}</div>
<div>
<button type="button" class="btn" onclick="toggleGroup('{{ $group }}')">Toggle group</button>
</div>
</div>
<div class="perm-list" id="group-{{ $group }}">
@foreach($groupPerms as $perm)
<label class="perm-item">
<input type="checkbox" name="permissions[]" value="{{ $perm->name }}"> {{ $perm->name }}
</label>
@endforeach
</div>
</div>
@endforeach
<div style="display:flex; justify-content:space-between; gap:.5rem;">
<button type="button" class="btn" onclick="showStage(3)">Back</button>
<button type="submit" class="btn primary">Create Staff</button>
</div>
</div>
</form>
</div>
<script>
function showStage(n){
document.querySelectorAll('.stage').forEach(s=>s.classList.remove('active'));
document.getElementById('stage-'+n).classList.add('active');
window.scrollTo({top:0, behavior:'smooth'});
}
function toggleGroup(group){
const el = document.getElementById('group-'+group);
if(!el) return;
const inputs = el.querySelectorAll('input[type="checkbox"]');
const anyUnchecked = Array.from(inputs).some(i => !i.checked);
inputs.forEach(i => i.checked = anyUnchecked);
}
</script>
@endsection

View File

@@ -0,0 +1,156 @@
@extends('admin.layouts.app')
@section('page-title', 'Account Dashboard')
@section('content')
<style>
.card { background:#fff; border:1px solid #eaeaea; padding:1rem; border-radius:6px; }
.field { margin-bottom:.8rem; }
label { display:block; font-weight:600; margin-bottom:.25rem; }
input[type="text"], input[type="email"], input[type="date"], input[type="password"], textarea, select {
width:100%; padding:.5rem; border:1px solid #ddd; border-radius:4px;
}
.perm-group { border:1px dashed #eee; padding:.6rem; margin-bottom:.6rem; border-radius:4px; }
.perm-list { display:flex; flex-wrap:wrap; gap:.5rem; }
.perm-item { min-width:200px; }
.btn { padding:.5rem .8rem; border-radius:5px; border:1px solid #ccc; background:#f6f6f6; cursor:pointer; }
.btn.primary { background:#0b74de; color:#fff; border-color:#0b74de; }
</style>
<div class="card">
<h3>Edit Staff {{ $staff->display_name ?? $staff->name }}</h3>
@if($errors->any())
<div style="background:#fff0f0; padding:.6rem; border:1px solid #f3c6c6; margin-bottom:1rem;">
<strong>There were some problems with your input:</strong>
<ul>
@foreach($errors->all() as $err)
<li>{{ $err }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('admin.staff.update', $staff->id) }}">
@csrf
@method('PUT')
<div class="field">
<label>Employee ID</label>
<input type="text" value="{{ $staff->employee_id }}" disabled>
</div>
<div class="field">
<label>Name *</label>
<input type="text" name="name" value="{{ old('name', $staff->name) }}" required>
</div>
<div class="field">
<label>Email *</label>
<input type="email" name="email" value="{{ old('email', $staff->email) }}" required>
</div>
<div class="field">
<label>Phone *</label>
<input type="text" name="phone" value="{{ old('phone', $staff->phone) }}" required>
</div>
<div class="field">
<label>Emergency Phone</label>
<input type="text" name="emergency_phone" value="{{ old('emergency_phone', $staff->emergency_phone) }}">
</div>
<div class="field">
<label>Address</label>
<textarea name="address" rows="3">{{ old('address', $staff->address) }}</textarea>
</div>
<hr>
<div class="field">
<label>Role</label>
<input type="text" name="role" value="{{ old('role', $staff->role) }}">
</div>
<div class="field">
<label>Department</label>
<input type="text" name="department" value="{{ old('department', $staff->department) }}">
</div>
<div class="field">
<label>Designation</label>
<input type="text" name="designation" value="{{ old('designation', $staff->designation) }}">
</div>
<div class="field">
<label>Joining Date</label>
<input type="date" name="joining_date" value="{{ old('joining_date', optional($staff->joining_date)->format('Y-m-d')) }}">
</div>
<div class="field">
<label>Status</label>
<select name="status">
<option value="active" {{ old('status', $staff->status)=='active'?'selected':'' }}>Active</option>
<option value="inactive" {{ old('status', $staff->status)=='inactive'?'selected':'' }}>Inactive</option>
</select>
</div>
<div class="field">
<label>Additional Info</label>
<textarea name="additional_info" rows="3">{{ old('additional_info', $staff->additional_info) }}</textarea>
</div>
<hr>
<div class="field">
<label>Username *</label>
<input type="text" name="username" value="{{ old('username', $staff->username) }}" required>
</div>
<div class="field">
<label>New Password (leave blank to keep existing)</label>
<input type="password" name="password" autocomplete="new-password">
</div>
<hr>
<div style="margin-bottom:.5rem; font-weight:700;">Permissions</div>
@foreach($permissions as $group => $groupPerms)
<div class="perm-group">
<div style="display:flex; justify-content:space-between; align-items:center;">
<div style="font-weight:600;">{{ ucfirst($group) }}</div>
<div>
<button type="button" class="btn" onclick="toggleGroup('{{ $group }}')">Toggle group</button>
</div>
</div>
<div class="perm-list" id="group-{{ $group }}">
@foreach($groupPerms as $perm)
<label class="perm-item">
<input type="checkbox" name="permissions[]" value="{{ $perm->name }}"
{{ in_array($perm->name, old('permissions', $staffPermissions)) ? 'checked' : '' }}>
{{ $perm->name }}
</label>
@endforeach
</div>
</div>
@endforeach
<div style="display:flex; justify-content:flex-end; gap:.5rem;">
<a href="{{ route('admin.staff.index') }}" class="btn">Cancel</a>
<button type="submit" class="btn primary">Update Staff</button>
</div>
</form>
</div>
<script>
function toggleGroup(group){
const el = document.getElementById('group-'+group);
if(!el) return;
const inputs = el.querySelectorAll('input[type="checkbox"]');
const anyUnchecked = Array.from(inputs).some(i => !i.checked);
inputs.forEach(i => i.checked = anyUnchecked);
}
</script>
@endsection

View File

@@ -0,0 +1,65 @@
@extends('admin.layouts.app')
@section('page-title', 'Account Dashboard')
@section('content')
<style>
.top-bar { display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem; }
.card { background:#fff; border:1px solid #e4e4e4; border-radius:6px; padding:1rem; box-shadow:0 1px 3px rgba(0,0,0,.03); }
table { width:100%; border-collapse:collapse; }
th, td { padding:.6rem .75rem; border-bottom:1px solid #f1f1f1; text-align:left; }
.btn { padding:.45rem .75rem; border-radius:4px; border:1px solid #ccc; background:#f7f7f7; cursor:pointer; }
.btn.primary { background:#0b74de; color:#fff; border-color:#0b74de; }
.actions a { margin-right:.5rem; color:#0b74de; text-decoration:none; }
.muted { color:#666; font-size:.95rem; }
</style>
<div class="top-bar">
<h2>Staff</h2>
<a href="{{ route('admin.staff.create') }}" class="btn primary">Add Staff</a>
</div>
<div class="card">
@if(session('success'))
<div style="padding:.5rem; background:#e6ffed; border:1px solid #b6f0c6; margin-bottom:1rem;">{{ session('success') }}</div>
@endif
<table>
<thead>
<tr>
<th>#</th>
<th>Employee ID</th>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Role</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@forelse($staff as $s)
<tr>
<td>{{ $s->id }}</td>
<td class="muted">{{ $s->employee_id }}</td>
<td>{{ $s->name }}</td>
<td>{{ $s->email }}</td>
<td>{{ $s->phone }}</td>
<td>{{ $s->role ?? '-' }}</td>
<td>{{ ucfirst($s->status) }}</td>
<td class="actions">
<a href="{{ route('admin.staff.edit', $s->id) }}">Edit</a>
<form action="{{ route('admin.staff.destroy', $s->id) }}" method="POST" style="display:inline" onsubmit="return confirm('Delete this staff?')">
@csrf
@method('DELETE')
<button class="btn" type="submit">Delete</button>
</form>
</td>
</tr>
@empty
<tr><td colspan="8" class="muted">No staff found.</td></tr>
@endforelse
</tbody>
</table>
</div>
@endsection

View File

@@ -1,975 +0,0 @@
@extends('admin.layouts.app')
@section('page-title', 'Shipment Details')
@section('content')
<style>
/* ===== ANIMATIONS ===== */
.fade-in {
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0px); }
}
/* ===== CARD STYLES ===== */
.card {
border-radius: 20px;
background: #ffffff;
box-shadow: 0 15px 35px rgba(13, 38, 76, 0.08);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.card:hover {
transform: translateY(-6px);
box-shadow: 0 20px 40px rgba(103, 126, 234, 0.15);
}
.card-header h5 {
margin: 0;
font-size: 20px;
font-weight: 800;
display: flex;
align-items: center;
gap: 10px;
}
/* ===== GLASSMORPHISM BUTTON ===== */
.glass-btn {
background: linear-gradient(
135deg,
rgba(103, 126, 234, 0.25) 0%,
rgba(118, 75, 162, 0.25) 100%
);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1.5px solid rgba(255, 255, 255, 0.4);
color: #ffffff !important;
padding: 12px 24px;
border-radius: 14px;
font-weight: 700;
display: inline-flex;
align-items: center;
gap: 10px;
text-decoration: none;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
0 8px 32px rgba(103, 126, 234, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.3),
inset 0 -1px 0 rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
z-index: 1;
}
.glass-btn::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
135deg,
rgba(103, 126, 234, 0.4) 0%,
rgba(118, 75, 162, 0.4) 100%
);
z-index: -1;
transition: opacity 0.4s ease;
opacity: 0;
}
.glass-btn:hover {
transform: translateY(-4px) scale(1.05);
box-shadow:
0 15px 40px rgba(103, 126, 234, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.4),
inset 0 -1px 0 rgba(0, 0, 0, 0.1);
border-color: rgba(255, 255, 255, 0.6);
background: linear-gradient(
135deg,
rgba(103, 126, 234, 0.15) 0%,
rgba(118, 75, 162, 0.15) 100%
);
}
.glass-btn:hover::before {
opacity: 1;
}
.glass-btn:active {
transform: translateY(-1px) scale(0.98);
transition: transform 0.1s ease;
}
.glass-btn i {
font-size: 18px;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
}
/* ===== HEADER STYLES ===== */
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
padding: 22px 28px;
border-radius: 20px 20px 0 0 !important;
position: relative;
overflow: hidden;
}
.card-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
to right,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0.05) 100%
);
pointer-events: none;
}
.title-row {
display: flex;
align-items: center;
gap: 12px;
z-index: 1;
}
.header-controls {
display: flex;
align-items: center;
gap: 12px;
z-index: 1;
}
.header-cancel-btn {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
color: rgba(255, 255, 255, 0.95);
border: 1.5px solid rgba(255, 255, 255, 0.3);
padding: 10px 14px;
border-radius: 12px;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.header-cancel-btn:hover {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
}
/* ===== INFO BLOCKS ===== */
.shipment-info-box {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 24px;
margin-bottom: 32px;
}
.shipment-info-item {
flex: 1;
min-width: 220px;
background: linear-gradient(145deg, #ffffff, #f8fafc);
padding: 24px;
border-radius: 16px;
box-shadow:
0 5px 20px rgba(0, 0, 0, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
text-align: center;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid rgba(255, 255, 255, 0.8);
position: relative;
overflow: hidden;
}
.shipment-info-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #667eea, #764ba2);
opacity: 0;
transition: opacity 0.4s ease;
}
.shipment-info-item:hover {
transform: translateY(-6px);
box-shadow:
0 15px 35px rgba(103, 126, 234, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
.shipment-info-item:hover::before {
opacity: 1;
}
.shipment-info-label {
color: #64748b;
font-weight: 600;
margin-bottom: 8px;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.shipment-info-value {
font-size: 24px;
font-weight: 800;
color: #1e293b;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
/* ===== TOTAL BOXES ===== */
.total-box {
background: linear-gradient(145deg, #ffffff, #f8fafc);
padding: 20px;
border-radius: 16px;
box-shadow:
0 5px 20px rgba(0, 0, 0, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
text-align: center;
min-width: 160px;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid rgba(255, 255, 255, 0.8);
position: relative;
overflow: hidden;
}
.total-box::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #667eea, #764ba2);
transform: translateX(-100%);
transition: transform 0.4s ease;
}
.total-box:hover {
transform: translateY(-5px) scale(1.03);
box-shadow:
0 15px 35px rgba(103, 126, 234, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
.total-box:hover::after {
transform: translateX(0);
}
.total-title {
font-size: 13px;
font-weight: 700;
color: #64748b;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 1px;
}
.total-value {
font-size: 22px;
font-weight: 900;
color: #1e293b;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
/* ===== TABLE STYLES ===== */
.table {
border-collapse: separate;
border-spacing: 0;
border-radius: 16px;
overflow: hidden;
background: #ffffff;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
}
.table thead th {
background: #667eea ;
color: #ffffff;
font-weight: 700;
padding: 18px;
white-space: nowrap;
border: none;
position: relative;
}
.table thead th::after {
content: '';
position: absolute;
bottom: 0;
left: 15px;
right: 15px;
height: 1px;
background: rgba(255, 255, 255, 0.3);
}
.table tbody tr {
transition: all 0.3s ease;
border-left: 3px solid transparent;
}
.table tbody tr:hover {
background: linear-gradient(90deg, rgba(103, 126, 234, 0.05), rgba(118, 75, 162, 0.05));
transform: translateX(4px);
border-left: 3px solid #667eea;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
}
.table tbody td {
padding: 16px;
vertical-align: middle;
white-space: nowrap;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
font-weight: 500;
}
/* ===== GLASSMORPHISM MODAL ===== */
#newOrderModal .modal-content {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border-radius: 24px;
border: 1.5px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.25),
inset 0 1px 0 rgba(255, 255, 255, 0.5);
overflow: hidden;
}
#newOrderModal .modal-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
padding: 24px 32px;
border-radius: 24px 24px 0 0;
position: relative;
overflow: hidden;
}
#newOrderModal .modal-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
to right,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0.05) 100%
);
}
#newOrderModal .modal-title {
color: #ffffff;
font-weight: 800;
font-size: 22px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
position: relative;
z-index: 1;
}
#newOrderModal .btn-close {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border-radius: 10px;
padding: 8px;
opacity: 0.9;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
#newOrderModal .btn-close:hover {
background: rgba(255, 255, 255, 0.3);
opacity: 1;
transform: rotate(90deg);
}
#newOrderModal .modal-body {
background: rgba(248, 250, 252, 0.7);
padding: 32px;
backdrop-filter: blur(10px);
}
#newOrderModal .modal-footer {
background: rgba(255, 255, 255, 0.8);
border-top: 1px solid rgba(255, 255, 255, 0.4);
padding: 24px 32px;
border-radius: 0 0 24px 24px;
backdrop-filter: blur(10px);
}
#newOrderModal h6 {
color: #1e293b;
font-weight: 700;
margin-bottom: 16px;
font-size: 16px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* ===== ORDER LIST ITEMS ===== */
#availableOrdersList,
#deletedOrdersList {
max-height: 300px;
overflow-y: auto;
border-radius: 16px;
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(10px);
border: 1.5px solid rgba(255, 255, 255, 0.5);
}
.list-group-item {
background: rgba(255, 255, 255, 0.7);
border: 1.5px solid rgba(255, 255, 255, 0.4);
margin-bottom: 8px;
border-radius: 12px !important;
padding: 16px;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.list-group-item:hover {
background: rgba(103, 126, 234, 0.1);
border-color: rgba(103, 126, 234, 0.3);
transform: translateX(4px);
box-shadow: 0 5px 15px rgba(103, 126, 234, 0.15);
}
.list-group-item strong {
color: #1e293b;
font-weight: 700;
}
.list-group-item .small {
color: #64748b;
font-size: 13px;
}
.order-checkbox {
width: 18px;
height: 18px;
border-radius: 6px;
border: 2px solid #cbd5e1;
cursor: pointer;
transition: all 0.3s ease;
}
.order-checkbox:checked {
background: linear-gradient(135deg, #667eea, #764ba2);
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(103, 126, 234, 0.2);
}
/* ===== MODAL BUTTONS ===== */
#newOrderModal .btn-secondary {
background: rgba(100, 116, 139, 0.2);
backdrop-filter: blur(10px);
border: 1.5px solid rgba(100, 116, 139, 0.3);
color: #64748b;
padding: 12px 28px;
border-radius: 12px;
font-weight: 600;
transition: all 0.3s ease;
}
#newOrderModal .btn-secondary:hover {
background: rgba(100, 116, 139, 0.3);
border-color: rgba(100, 116, 139, 0.5);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(100, 116, 139, 0.2);
}
#newOrderModal .btn-success {
background: linear-gradient(
135deg,
rgba(34, 197, 94, 0.25) 0%,
rgba(21, 128, 61, 0.25) 100%
);
backdrop-filter: blur(12px);
border: 1.5px solid rgba(34, 197, 94, 0.4);
color: #166534;
padding: 12px 28px;
border-radius: 12px;
font-weight: 700;
transition: all 0.3s ease;
box-shadow:
0 8px 25px rgba(34, 197, 94, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
}
#newOrderModal .btn-success:hover {
background: linear-gradient(
135deg,
rgba(34, 197, 94, 0.35) 0%,
rgba(21, 128, 61, 0.35) 100%
);
border-color: rgba(34, 197, 94, 0.6);
color: #14532d;
transform: translateY(-2px);
box-shadow:
0 12px 30px rgba(34, 197, 94, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
/* ===== SCROLLBAR STYLING ===== */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #5a6fe8, #6a42a0);
}
/* ===== ACTION BUTTONS ===== */
.btn-secondary {
background: linear-gradient(135deg, #94a3b8, #64748b);
border: none;
color: white;
padding: 12px 28px;
border-radius: 12px;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 6px 20px rgba(100, 116, 139, 0.2);
}
.btn-secondary:hover {
background: linear-gradient(135deg, #8492a6, #565e6e);
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(100, 116, 139, 0.3);
color: white;
}
/* ===== RESPONSIVE ADJUSTMENTS ===== */
@media (max-width: 768px) {
.shipment-info-item,
.total-box {
min-width: 100%;
}
.card-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.header-controls {
width: 100%;
justify-content: space-between;
}
.glass-btn {
padding: 10px 20px;
font-size: 14px;
}
}
</style>
<div class="container-fluid py-4">
<div class="card mb-4">
<div class="card-header">
<div class="title-row">
<h5 class="mb-0"><i class="bi bi-truck me-2"></i>Shipment Details</h5>
<!-- <small class="text-white-50" style="margin-left:8px; font-weight:600;">{{ $shipment->shipment_id ?? '' }}</small> -->
</div>
<div class="header-controls">
<!-- Add Order button (top-right) with glass effect -->
@if($mode === 'edit' && $shipment->status === 'pending')
<a href="#" data-bs-toggle="modal" data-bs-target="#newOrderModal" class="glass-btn">
<i class="bi bi-plus-circle"></i> Add Order
</a>
@endif
<!-- Cancel/close: goes back to shipments list -->
<a href="{{ route('admin.shipments') }}" class="btn header-cancel-btn" title="Cancel / Back">
<i class="bi bi-x-lg"></i>
</a>
</div>
</div>
<div class="card-body">
<!-- ================================ -->
<!-- SHIPMENT MAIN DETAILS -->
<!-- ================================ -->
<div class="shipment-info-box">
<div class="shipment-info-item">
<div class="shipment-info-label">Shipment ID</div>
<div class="shipment-info-value">{{ $shipment->shipment_id }}</div>
</div>
<div class="shipment-info-item">
<div class="shipment-info-label">Status</div>
<div class="shipment-info-value text-capitalize">
{{ str_replace('_', ' ', $shipment->status) }}
</div>
</div>
<div class="shipment-info-item">
<div class="shipment-info-label">Shipment Date</div>
<div class="shipment-info-value">
{{ \Carbon\Carbon::parse($shipment->shipment_date)->format('d M Y') }}
</div>
</div>
<div class="shipment-info-item">
<div class="shipment-info-label">Total Orders</div>
<div class="shipment-info-value">{{ $orders->count() }}</div>
</div>
</div>
<!-- ================================ -->
<!-- SHIPMENT TOTAL BLOCKS -->
<!-- ================================ -->
<h5 class="fw-bold mb-3 mt-4" style="color: #1e293b;">Shipment Summary</h5>
<div class="d-flex flex-wrap gap-3">
<div class="total-box">
<div class="total-title">TOTAL CTN</div>
<div class="total-value">{{ $shipment->total_ctn }}</div>
</div>
<div class="total-box">
<div class="total-title">TOTAL QTY</div>
<div class="total-value">{{ $shipment->total_qty }}</div>
</div>
<div class="total-box">
<div class="total-title">TOTAL CBM</div>
<div class="total-value">{{ $shipment->total_cbm }}</div>
</div>
<div class="total-box">
<div class="total-title">TOTAL KG</div>
<div class="total-value">{{ $shipment->total_kg }}</div>
</div>
<div class="total-box">
<div class="total-title">TOTAL TTL QTY</div>
<div class="total-value">{{ $shipment->total_ttl_qty }}</div>
</div>
<div class="total-box">
<div class="total-title">TOTAL TTL CBM</div>
<div class="total-value">{{ $shipment->total_ttl_cbm }}</div>
</div>
<div class="total-box">
<div class="total-title">TOTAL TTL KG</div>
<div class="total-value">{{ $shipment->total_ttl_kg }}</div>
</div>
<div class="total-box">
<div class="total-title">TOTAL AMOUNT</div>
<div class="total-value text-success">
{{ number_format($shipment->total_amount, 2) }}
</div>
</div>
</div>
<!-- ================================ -->
<!-- ORDERS TABLE LIST -->
<!-- ================================ -->
<h5 class="fw-bold mb-3 mt-5" style="color: #1e293b;">Orders in This Shipment</h5>
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Order ID</th>
<th>Origin</th>
<th>Destination</th>
<th>CTN</th>
<th>QTY</th>
<th>TTL/QTY</th>
<th>CBM</th>
<th>TTL CBM</th>
<th>KG</th>
<th>TTL KG</th>
<th>Amount ()</th>
<!-- Conditionally show Delete column header -->
@if($mode === 'edit' && $shipment->status === 'pending')
<th>Delete</th>
@endif
</tr>
</thead>
<tbody>
@foreach($orders as $order)
<tr>
<td class="fw-bold text-primary">{{ $order->order_id }}</td>
<td>{{ $order->origin }}</td>
<td>{{ $order->destination }}</td>
<td>{{ $order->ctn }}</td>
<td>{{ $order->qty }}</td>
<td>{{ $order->ttl_qty }}</td>
<td>{{ $order->cbm }}</td>
<td>{{ $order->ttl_cbm }}</td>
<td>{{ $order->kg }}</td>
<td>{{ $order->ttl_kg }}</td>
<td class="fw-bold text-success">
{{ number_format($order->ttl_amount, 2) }}
</td>
<!-- Conditionally show Delete button -->
@if($mode === 'edit' && $shipment->status === 'pending')
<td>
<form method="POST" action="{{ route('admin.shipments.removeOrder') }}" onsubmit="return confirmDelete()">
@csrf
<input type="hidden" name="shipment_id" value="{{ $shipment->id }}">
<input type="hidden" name="order_id" value="{{ $order->id }}">
<button type="submit" class="btn btn-danger btn-sm" style="border-radius: 10px; padding: 8px 16px;">
<i class="bi bi-trash"></i> Delete
</button>
</form>
</td>
@endif
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- Action Buttons -->
<div class="d-flex justify-content-between mt-4">
<!-- <a href="{{ route('admin.shipments') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Back to Shipments
</a>
-->
@if($mode === 'edit' && $shipment->status === 'pending')
<div>
<!-- <a href="{{ route('admin.shipments.view', ['id' => $shipment->id, 'mode' => 'edit']) }}"
class="btn btn-warning me-2">
<i class="bi bi-pencil"></i> Edit Shipment
</a> -->
<!-- <a href="{{ route('admin.shipments.view', ['id' => $shipment->id, 'mode' => 'view']) }}"
class="btn btn-info">
<i class="bi bi-eye"></i> View Mode
</a> -->
</div>
@elseif($shipment->status === 'pending')
<div>
<!-- <a href="{{ route('admin.shipments.view', ['id' => $shipment->id, 'mode' => 'edit']) }}"
class="btn btn-primary">
<i class="bi bi-pencil"></i> Edit Shipment
</a> -->
</div>
@endif
</div>
</div>
</div>
<!-- New Order Modal -->
<div class="modal fade" id="newOrderModal" tabindex="-1" aria-labelledby="newOrderModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="newOrderModalLabel">Add Orders to Shipment</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row mb-3">
<div class="col-md-6">
<h6>Deleted Orders (recently removed)</h6>
<div id="deletedOrdersList" class="list-group"></div>
</div>
<div class="col-md-6">
<h6>Available Orders</h6>
<div id="availableOrdersList" class="list-group"></div>
</div>
</div>
<div class="mt-3">
<small class="text-muted">Select orders to add back to this shipment, then click "Add Selected".</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button id="addSelectedOrdersBtn" type="button" class="btn btn-success">Add Selected</button>
</div>
</div>
</div>
</div>
</div>
<script>
function confirmDelete() {
return confirm('Are you sure you want to remove this order from the shipment?');
}
</script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const csrfToken = '{{ csrf_token() }}';
const shipmentId = {{ $shipment->id }};
let availableOrders = @json($availableOrders ?? []);
let deletedOrders = [];
function renderOrdersList(containerId, orders) {
const container = document.getElementById(containerId);
container.innerHTML = '';
if (!orders.length) {
container.innerHTML = '<div class="text-muted py-3">No orders</div>';
return;
}
orders.forEach(order => {
const item = document.createElement('label');
item.className = 'list-group-item d-flex justify-content-between align-items-center';
item.innerHTML = `
<div>
<input type="checkbox" class="me-2 order-checkbox" data-order='${JSON.stringify(order)}'>
<strong>${order.order_id}</strong>
<div class="small text-muted"> ${order.origin} ${order.destination} ${order.qty} qty ${parseFloat(order.ttl_amount).toLocaleString('en-IN', {minimumFractionDigits:2})}</div>
</div>
<div class="small text-muted">ID: ${order.id}</div>
`;
container.appendChild(item);
});
}
renderOrdersList('availableOrdersList', availableOrders);
renderOrdersList('deletedOrdersList', deletedOrders);
// Intercept remove-order forms (update selector if form action different)
document.querySelectorAll('form[action="{{ route('admin.shipments.removeOrder') }}"]').forEach(form => {
form.addEventListener('submit', function(e) {
e.preventDefault();
if (!confirm('Are you sure you want to remove this order from the shipment?')) return;
const formData = new FormData(this);
fetch(this.action, {
method: 'POST',
headers: {'X-CSRF-TOKEN': csrfToken, 'Accept': 'application/json'},
body: formData
})
.then(res => res.json())
.then(data => {
if (!data.success) { alert(data.message || 'Failed'); return; }
// Remove row from DOM
const row = this.closest('tr');
// Build deleted order object from row cells if present
const orderObj = {
id: formData.get('order_id'),
order_id: row ? row.querySelector('td:first-child')?.innerText.trim() : ('Order-'+formData.get('order_id')),
origin: row ? row.cells[1]?.innerText.trim() : '',
destination: row ? row.cells[2]?.innerText.trim() : '',
qty: row ? row.cells[4]?.innerText.trim() : '',
ttl_amount: row ? row.cells[10]?.innerText.replace('₹','').replace(',','').trim() : 0
};
if (row) row.remove();
deletedOrders.unshift(orderObj);
renderOrdersList('deletedOrdersList', deletedOrders);
// Optional: update totals by refetching shipment summary endpoint or reload
if (data.shipment) updateTotalsInDOM(data.shipment);
})
.catch(err => {
console.error(err);
alert('Remove failed.');
});
});
});
// Open modal from add-order button
document.querySelectorAll('.glass-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
renderOrdersList('availableOrdersList', availableOrders);
renderOrdersList('deletedOrdersList', deletedOrders);
const modal = new bootstrap.Modal(document.getElementById('newOrderModal'));
modal.show();
});
});
// Add selected orders
document.getElementById('addSelectedOrdersBtn').addEventListener('click', function() {
const checked = Array.from(document.querySelectorAll('.order-checkbox:checked'));
if (!checked.length) { alert('Please select orders'); return; }
const orderIds = checked.map(cb => JSON.parse(cb.getAttribute('data-order')).id);
fetch(`/admin/shipments/${shipmentId}/add-orders`, {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken, 'Accept': 'application/json'},
body: JSON.stringify({ order_ids: orderIds })
})
.then(r => r.json())
.then(data => {
if (!data.success) { alert(data.message || 'Failed'); return; }
// Update orders table — simplest approach: reload page to reflect DB
// But we can also append rows from data.orders (if provided)
location.reload();
})
.catch(err => {
console.error(err);
alert('Add failed.');
});
});
function updateTotalsInDOM(shipment) {
try {
if (document.getElementById('total_qty')) {
document.getElementById('total_qty').innerText = shipment.total_qty;
}
if (document.getElementById('total_amount')) {
document.getElementById('total_amount').innerText = '₹' + parseFloat(shipment.total_amount).toLocaleString('en-IN', {minimumFractionDigits:2});
}
// add other totals similarly...
} catch(e) { console.warn(e); }
}
});
document.addEventListener('hidden.bs.modal', function (event) {
document.body.classList.remove('modal-open');
let backdrop = document.querySelector('.modal-backdrop');
if (backdrop) backdrop.remove();
});
</script>
@endsection

View File

@@ -0,0 +1,128 @@
@extends('admin.layouts.app')
@section('page-title', 'Shipment Preview')
@section('content')
<div class="container py-4">
<div class="card shadow-sm border-0">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="bi bi-box-seam me-2"></i>
Shipment Preview: {{ $shipment->shipment_id }}
</h5>
</div>
<div class="card-body">
<!-- Dummy Info -->
<div class="alert alert-info">
<strong>{{ $dummyData['title'] }}</strong><br>
Generated On: {{ $dummyData['generated_on'] }} <br>
Note: {{ $dummyData['note'] }}
</div>
<!-- Shipment Basic Details -->
<h5 class="fw-bold mt-3">Shipment Details</h5>
<table class="table table-bordered">
<tr>
<th>Shipment ID</th>
<td>{{ $shipment->shipment_id }}</td>
</tr>
<tr>
<th>Origin</th>
<td>{{ $shipment->origin }}</td>
</tr>
<tr>
<th>Destination</th>
<td>{{ $shipment->destination }}</td>
</tr>
<tr>
<th>Status</th>
<td>{{ ucfirst($shipment->status) }}</td>
</tr>
<tr>
<th>Date</th>
<td>{{ \Carbon\Carbon::parse($shipment->shipment_date)->format('d M Y') }}</td>
</tr>
</table>
<!-- Orders in Shipment -->
<h5 class="fw-bold mt-4">Orders Contained in Shipment</h5>
@if($shipment->orders->isEmpty())
<p class="text-muted">No orders found.</p>
@else
<div class="table-responsive">
<table class="table table-hover table-striped">
<thead class="table-dark">
<tr>
<th>Order ID</th>
<th>Origin</th>
<th>Destination</th>
<th>CTN</th>
<th>QTY</th>
<th>TTL/QTY</th>
<th>KG</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
@foreach($shipment->orders as $order)
<tr>
<td>{{ $order->order_id }}</td>
<td>{{ $order->origin }}</td>
<td>{{ $order->destination }}</td>
<td>{{ $order->ctn }}</td>
<td>{{ $order->qty }}</td>
<td>{{ $order->ttl_qty }}</td>
<td>{{ $order->ttl_kg }}</td>
<td>{{ number_format($order->ttl_amount, 2) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
<!-- Shipment Totals -->
<h5 class="fw-bold mt-4">Shipment Totals</h5>
<table class="table table-bordered">
<tr>
<th>Total CTN</th>
<td>{{ $shipment->total_ctn }}</td>
</tr>
<tr>
<th>Total Quantity</th>
<td>{{ $shipment->total_qty }}</td>
</tr>
<tr>
<th>Total TTL Quantity</th>
<td>{{ $shipment->total_ttl_qty }}</td>
</tr>
<tr>
<th>Total CBM</th>
<td>{{ $shipment->total_cbm }}</td>
</tr>
<tr>
<th>Total KG</th>
<td>{{ $shipment->total_kg }}</td>
</tr>
<tr>
<th>Total Amount</th>
<td class="fw-bold text-success">
{{ number_format($shipment->total_amount, 2) }}
</td>
</tr>
</table>
<a href="{{ route('admin.shipments') }}" class="btn btn-secondary mt-3">
Back to Shipments
</a>
</div>
</div>
</div>
@endsection

View File

@@ -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');
});
@@ -147,47 +140,37 @@ Route::prefix('admin')
Route::delete('/orders/{id}/delete', [AdminOrderController::class, 'destroy'])
->name('admin.orders.destroy');
// ---------------------------
// SHIPMENTS (FIXED ROUTES)
// ---------------------------
// ---------------------------
// SHIPMENTS (FIXED ROUTES)
// ---------------------------
Route::get('/shipments', [ShipmentController::class, 'index'])
->name('admin.shipments');
Route::post('/shipments', [ShipmentController::class, 'store'])
->name('admin.shipments.store');
Route::post('/shipments/update-status', [ShipmentController::class, 'updateStatus'])
->name('admin.shipments.updateStatus');
// Get shipment orders for modal (AJAX)
Route::get('/shipments/{id}/orders', [ShipmentController::class, 'getShipmentOrders'])
->name('admin.shipments.orders');
// Get shipment details for edit (AJAX)
Route::get('/shipments/{id}', [ShipmentController::class, 'show'])
->name('admin.shipments.show');
// Shipment Update
Route::put('/shipments/{id}', [ShipmentController::class, 'update'])
->name('admin.shipments.update');
// Shipment Delete
Route::delete('/shipments/{id}', [ShipmentController::class, 'destroy'])
->name('admin.shipments.destroy');
// View shipment MUST be before /shipments/{id}
Route::get('/shipments/view/{id}', [ShipmentController::class, 'show'])
->name('admin.shipments.view');
Route::get('/shipment/dummy/{id}', [ShipmentController::class, 'dummy'])
->name('admin.shipments.dummy');
// List shipments
Route::get('/shipments', [ShipmentController::class, 'index'])
->name('admin.shipments');
// Create shipment
Route::post('/shipments', [ShipmentController::class, 'store'])
->name('admin.shipments.store');
// Update status
Route::post('/shipments/update-status', [ShipmentController::class, 'updateStatus'])
->name('admin.shipments.updateStatus');
// Shipment orders (AJAX)
Route::get('/shipments/{id}/orders', [ShipmentController::class, 'getShipmentOrders'])
->name('admin.shipments.orders');
// Shipment update
Route::put('/shipments/{id}', [ShipmentController::class, 'update'])
->name('admin.shipments.update');
// Shipment delete
Route::delete('/shipments/{id}', [ShipmentController::class, 'destroy'])
->name('admin.shipments.destroy');
// Remove order
Route::post('/shipments/remove-order', [ShipmentController::class, 'removeOrder'])
->name('admin.shipments.removeOrder');
Route::post('/shipments/add-order', [ShipmentController::class, 'addOrder'])
->name('admin.shipments.addOrder');
Route::post('/shipments/{id}/add-orders', [ShipmentController::class, 'addOrders'])
->name('admin.shipments.addOrders');
// ---------------------------
// INVOICES
@@ -301,7 +284,21 @@ Route::prefix('admin')
Route::get('/admin/orders/download/excel', [AdminOrderController::class, 'downloadExcel'])
->name('admin.orders.download.excel');
Route::prefix('admin/account')->middleware('auth:admin')->name('admin.account.')->group(function () {
Route::post('/toggle-payment', [AdminAccountController::class, 'togglePayment'])->name('toggle');
});
//---------------------------
//Edit Button Route
//---------------------------
// protected admin routes
Route::middleware(['auth:admin'])
->prefix('admin')
->name('admin.')
->group(function () {
// staff resource
Route::resource('staff', AdminStaffController::class);
});