Resolve merge conflicts

This commit is contained in:
Utkarsh Khedkar
2026-02-17 14:44:47 +05:30
84 changed files with 16116 additions and 5486 deletions

View File

@@ -6,10 +6,11 @@ use App\Models\Order;
use Illuminate\Http\Request;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Carbon\Carbon;
class OrdersExport implements FromCollection, WithHeadings
{
protected $request;
protected Request $request;
public function __construct(Request $request)
{
@@ -18,61 +19,99 @@ class OrdersExport implements FromCollection, WithHeadings
private function buildQuery()
{
$query = Order::with(['markList', 'invoice', 'shipments']);
$query = Order::query()->with([
'markList',
'invoice',
'shipments',
]);
// SEARCH
if ($this->request->filled('search')) {
$search = $this->request->search;
$query->where(function($q) use ($search) {
$q->where('order_id', 'like', "%{$search}%")
->orWhereHas('markList', function($q2) use ($search) {
$search = trim($this->request->search);
$query->where(function ($q) use ($search) {
$q->where('orders.order_id', 'like', "%{$search}%")
->orWhereHas('markList', function ($q2) use ($search) {
$q2->where('company_name', 'like', "%{$search}%")
->orWhere('customer_id', 'like', "%{$search}%");
->orWhere('customer_id', 'like', "%{$search}%")
->orWhere('origin', 'like', "%{$search}%")
->orWhere('destination', 'like', "%{$search}%");
})
->orWhereHas('invoice', function($q3) use ($search) {
->orWhereHas('invoice', function ($q3) use ($search) {
$q3->where('invoice_number', 'like', "%{$search}%");
})
->orWhereHas('shipments', function ($q4) use ($search) {
// ✅ FIXED
$q4->where('shipments.shipment_id', 'like', "%{$search}%");
});
});
}
// INVOICE STATUS
// INVOICE STATUS (FIXED)
if ($this->request->filled('status')) {
$query->whereHas('invoice', function($q) {
$q->where('status', $this->request->status);
$query->where(function ($q) {
$q->whereHas('invoice', function ($q2) {
$q2->where('status', $this->request->status);
})
->orWhereDoesntHave('invoice');
});
}
// SHIPMENT STATUS (FIXED)
if ($this->request->filled('shipment')) {
$query->whereHas('shipments', function($q) {
$q->where('status', $this->request->shipment);
$query->where(function ($q) {
$q->whereHas('shipments', function ($q2) {
$q2->where('status', $this->request->shipment);
})
->orWhereDoesntHave('shipments');
});
}
return $query->latest('id');
// DATE RANGE
if ($this->request->filled('from_date')) {
$query->whereDate('orders.created_at', '>=', $this->request->from_date);
}
if ($this->request->filled('to_date')) {
$query->whereDate('orders.created_at', '<=', $this->request->to_date);
}
return $query->latest('orders.id');
}
public function collection()
{
$orders = $this->buildQuery()->get();
return $this->buildQuery()->get()->map(function ($order) {
// Map to simple array rows suitable for Excel
return $orders->map(function($order) {
$mark = $order->markList;
$invoice = $order->invoice;
$shipment = $order->shipments->first() ?? null;
$mark = $order->markList;
$invoice = $order->invoice;
$shipment = $order->shipments->first();
return [
'Order ID' => $order->order_id,
'Shipment ID' => $shipment->shipment_id ?? '-',
'Customer ID' => $mark->customer_id ?? '-',
'Company' => $mark->company_name ?? '-',
'Origin' => $mark->origin ?? $order->origin ?? '-',
'Destination' => $mark->destination ?? $order->destination ?? '-',
'Order Date' => $order->created_at ? $order->created_at->format('d-m-Y') : '-',
'Invoice No' => $invoice->invoice_number ?? '-',
'Invoice Date' => $invoice?->invoice_date ? \Carbon\Carbon::parse($invoice->invoice_date)->format('d-m-Y') : '-',
'Amount' => $invoice?->final_amount ? number_format($invoice->final_amount, 2) : '-',
'Amount + GST' => $invoice?->final_amount_with_gst ? number_format($invoice->final_amount_with_gst, 2) : '-',
'Invoice Status' => $invoice->status ? ucfirst($invoice->status) : 'Pending',
'Shipment Status' => $shipment?->status ? ucfirst(str_replace('_', ' ', $shipment->status)) : 'Pending',
'Order ID' => $order->order_id ?? '-',
'Shipment ID' => $shipment?->shipment_id ?? '-',
'Customer ID' => $mark?->customer_id ?? '-',
'Company' => $mark?->company_name ?? '-',
'Origin' => $mark?->origin ?? $order->origin ?? '-',
'Destination' => $mark?->destination ?? $order->destination ?? '-',
'Order Date' => $order->created_at
? $order->created_at->format('d-m-Y')
: '-',
'Invoice No' => $invoice?->invoice_number ?? '-',
'Invoice Date' => $invoice?->invoice_date
? Carbon::parse($invoice->invoice_date)->format('d-m-Y')
: '-',
'Amount' => $invoice?->final_amount !== null
? number_format($invoice->final_amount, 2)
: '0.00',
'Amount + GST' => $invoice?->final_amount_with_gst !== null
? number_format($invoice->final_amount_with_gst, 2)
: '0.00',
'Invoice Status' => ucfirst($invoice?->status ?? 'pending'),
'Shipment Status' => ucfirst(str_replace('_', ' ', $shipment?->status ?? 'pending')),
];
});
}

View File

@@ -3,14 +3,22 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
<<<<<<< HEAD
use App\Models\SupportTicket;
use App\Models\ChatMessage;
use App\Events\NewChatMessage;
use Illuminate\Http\Request;
=======
use Illuminate\Http\Request;
use App\Models\SupportTicket;
use App\Models\ChatMessage;
use App\Events\NewChatMessage;
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
class AdminChatController extends Controller
{
/**
<<<<<<< HEAD
* Page 1: List all customer chat tickets
*/
public function index()
@@ -23,11 +31,31 @@ class AdminChatController extends Controller
return view('admin.chat_support', compact('tickets'));
}
=======
* Page 1: List all active user chats
*/
public function index()
{
$tickets = SupportTicket::with('user')
->withCount([
'messages as unread_count' => function ($q) {
$q->where('sender_type', \App\Models\User::class)
->where('read_by_admin', false);
}
])
->orderBy('updated_at', 'desc')
->get();
return view('admin.chat_support', compact('tickets'));
}
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
/**
* Page 2: Open chat window for a specific user
*/
public function openChat($ticketId)
<<<<<<< HEAD
{
$ticket = SupportTicket::with('user')->findOrFail($ticketId);
$messages = ChatMessage::where('ticket_id', $ticketId)
@@ -40,6 +68,28 @@ class AdminChatController extends Controller
/**
* Admin sends a message to the user (FIXED - LIVE CHAT)
=======
{
$ticket = SupportTicket::with('user')->findOrFail($ticketId);
// ✅ MARK USER MESSAGES AS READ FOR ADMIN
ChatMessage::where('ticket_id', $ticketId)
->where('sender_type', \App\Models\User::class)
->where('read_by_admin', false)
->update(['read_by_admin' => true]);
$messages = ChatMessage::where('ticket_id', $ticketId)
->orderBy('created_at', 'asc')
->with('sender')
->get();
return view('admin.chat_window', compact('ticket', 'messages'));
}
/**
* Admin sends a message to the user
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
*/
public function sendMessage(Request $request, $ticketId)
{
@@ -56,6 +106,12 @@ class AdminChatController extends Controller
'sender_id' => $admin->id,
'sender_type' => \App\Models\Admin::class,
'message' => $request->message,
<<<<<<< HEAD
=======
'read_by_admin' => true,
'read_by_user' => false,
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
];
// File Upload
@@ -69,14 +125,28 @@ class AdminChatController extends Controller
$message = ChatMessage::create($data);
$message->load('sender');
<<<<<<< HEAD
\Log::info("DEBUG: ChatController sendMessage called", [
=======
\Log::info("DEBUG: ChatController sendMessage called", [
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
'ticket_id' => $ticketId,
'payload' => $request->all()
]);
<<<<<<< HEAD
// 🔥 LIVE CHAT - Queue bypass (100% working)
broadcast(new NewChatMessage($message))->toOthers();
=======
// Broadcast real-time
broadcast(new NewChatMessage($message));
\Log::info("DEBUG: ChatController sendMessage called 79", [
'ticket_id' => $ticketId,
'payload' => $request->all()
]);
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
return response()->json([
'success' => true,
'message' => $message

View File

@@ -19,32 +19,37 @@ class AdminCustomerController extends Controller
$search = $request->search;
$status = $request->status;
$query = User::with(['marks', 'orders'])->orderBy('id', 'desc');
$query = User::with([
'marks',
'orders',
'invoices.installments' // 🔥 IMPORTANT
])->orderBy('id', 'desc');
// SEARCH FILTER
if (!empty($search)) {
$query->where(function ($q) use ($search) {
$q->where('customer_name', 'like', "%$search%")
->orWhere('email', 'like', "%$search%")
->orWhere('mobile_no', 'like', "%$search%")
->orWhere('customer_id', 'like', "%$search%");
->orWhere('email', 'like', "%$search%")
->orWhere('mobile_no', 'like', "%$search%")
->orWhere('customer_id', 'like', "%$search%");
});
}
// STATUS FILTER
if (!empty($status) && in_array($status, ['active', 'inactive'])) {
$query->where('status', $status);
}
// Get all customers for statistics (without pagination)
$allCustomers = $query->get();
// Get paginated customers for the table (10 per page)
$customers = $query->paginate(10);
return view('admin.customers', compact('customers', 'allCustomers', 'search', 'status'));
return view('admin.customers', compact(
'customers',
'allCustomers',
'search',
'status'
));
}
// ---------------------------------------------------------
// SHOW ADD CUSTOMER FORM
// ---------------------------------------------------------
@@ -106,20 +111,36 @@ class AdminCustomerController extends Controller
// VIEW CUSTOMER FULL DETAILS
// ---------------------------------------------------------
public function view($id)
{
$customer = User::with(['marks', 'orders'])->findOrFail($id);
{
$customer = User::with([
'marks',
'orders',
'invoices.installments'
])->findOrFail($id);
$totalOrders = $customer->orders->count();
$totalAmount = $customer->orders->sum('ttl_amount');
$recentOrders = $customer->orders()->latest()->take(5)->get();
// Orders
$totalOrders = $customer->orders->count();
$totalOrderAmount = $customer->orders->sum('ttl_amount');
// Invoices (PAYABLE)
$totalPayable = $customer->invoices->sum('final_amount_with_gst');
// Paid via installments
$totalPaid = $customer->invoiceInstallments->sum('amount');
// Remaining
$totalRemaining = max($totalPayable - $totalPaid, 0);
return view('admin.customers_view', compact(
'customer',
'totalOrders',
'totalOrderAmount',
'totalPayable',
'totalPaid',
'totalRemaining'
));
}
return view('admin.customers_view', compact(
'customer',
'totalOrders',
'totalAmount',
'recentOrders'
));
}
// ---------------------------------------------------------
// TOGGLE STATUS ACTIVE / INACTIVE

View File

@@ -32,7 +32,7 @@ class AdminInvoiceController extends Controller
$invoice = Invoice::with(['items', 'customer', 'container'])->findOrFail($id);
$shipment = null;
return view('admin.popup_invoice', compact('invoice', 'shipment'));
return view('admin.popup_invoice', compact('invoice', 'shipment'));
}
// -------------------------------------------------------------
@@ -43,7 +43,14 @@ class AdminInvoiceController extends Controller
$invoice = Invoice::with(['items', 'customer', 'container'])->findOrFail($id);
$shipment = null;
return view('admin.invoice_edit', compact('invoice', 'shipment'));
// ADD THIS SECTION: Calculate customer's total due across all invoices
$customerTotalDue = Invoice::where('customer_id', $invoice->customer_id)
->where('status', '!=', 'cancelled')
->where('status', '!=', 'void')
->sum('final_amount_with_gst');
// Pass the new variable to the view
return view('admin.invoice_edit', compact('invoice', 'shipment', 'customerTotalDue'));
}
// -------------------------------------------------------------
@@ -250,6 +257,17 @@ class AdminInvoiceController extends Controller
$invoice->update(['pdf_path' => 'invoices/' . $fileName]);
}
public function downloadInvoice($id)
{
$invoice = Invoice::findOrFail($id);
// ALWAYS regenerate to reflect latest HTML/CSS
$this->generateInvoicePDF($invoice);
$invoice->refresh();
return response()->download(public_path($invoice->pdf_path));
}
// -------------------------------------------------------------
// INSTALLMENTS (ADD)
// -------------------------------------------------------------
@@ -285,6 +303,8 @@ class AdminInvoiceController extends Controller
if ($newPaid >= $invoice->final_amount_with_gst) {
$invoice->update(['status' => 'paid']);
$this->generateInvoicePDF($invoice);
}
return response()->json([
@@ -315,6 +335,8 @@ class AdminInvoiceController extends Controller
if ($remaining > 0 && $invoice->status === 'paid') {
$invoice->update(['status' => 'pending']);
$this->generateInvoicePDF($invoice);
}
return response()->json([

File diff suppressed because it is too large Load Diff

View File

@@ -55,6 +55,7 @@ class AdminStaffController extends Controller
DB::beginTransaction();
try {
// 1⃣ Create staff WITHOUT employee_id (ID not available yet)
$admin = Admin::create([
'name' => $request->name,
'email' => $request->email,
@@ -69,23 +70,33 @@ class AdminStaffController extends Controller
'status' => $request->status,
'additional_info' => $request->additional_info,
'username' => $request->username,
// username may be NULL here
'username' => $request->username ?: null,
'password' => Hash::make($request->password),
'type' => 'staff',
]);
// Generate EMPLOYEE ID using admin ID (safe)
// 2 Generate EMPLOYEE ID
$employeeId = 'EMP' . str_pad($admin->id, 4, '0', STR_PAD_LEFT);
$admin->update(['employee_id' => $employeeId]);
// Assign permissions (if any)
// 3⃣ Auto-generate username if left blank
$username = $request->username ?: strtolower($employeeId);
// 4⃣ Update employee_id + username together
$admin->update([
'employee_id' => $employeeId,
'username' => $username,
]);
// 5⃣ Assign permissions (if any)
if ($request->permissions) {
$admin->givePermissionTo($request->permissions);
}
DB::commit();
return redirect()->route('admin.staff.index')
return redirect()
->route('admin.staff.index')
->with('success', 'Staff created successfully.');
} catch (\Exception $e) {
@@ -94,6 +105,7 @@ class AdminStaffController extends Controller
}
}
public function edit($id)
{
$staff = Admin::where('type', 'staff')->findOrFail($id);

View File

@@ -20,7 +20,11 @@ class ShipmentController extends Controller
$usedOrderIds = ShipmentItem::pluck('order_id')->toArray();
// 2) Load available orders (not used in any shipment)
$availableOrders = Order::whereNotIn('id', $usedOrderIds)->get();
$availableOrders = Order::whereNotIn('id', $usedOrderIds)
->where('status', '!=', 'order_placed')
->get();
// 3) Load all shipments for listing
$shipments = Shipment::latest()->get();
@@ -65,6 +69,16 @@ class ShipmentController extends Controller
// CALCULATE TOTALS
// -----------------------------
$orders = Order::whereIn('id', $request->order_ids)->get();
foreach ($orders as $order) {
if ($order->status === 'order_placed') {
return back()->with(
'error',
"Order {$order->order_id} is not ready for shipment"
);
}
}
$total_ctn = $orders->sum('ctn');
$total_qty = $orders->sum('qty');
@@ -82,7 +96,7 @@ class ShipmentController extends Controller
'shipment_id' => $newShipmentId,
'origin' => $request->origin,
'destination' => $request->destination,
'status' => Shipment::STATUS_PENDING,
'status' => Shipment::STATUS_SHIPMENT_READY,
'shipment_date' => $request->shipment_date,
'total_ctn' => $total_ctn,
@@ -135,29 +149,35 @@ class ShipmentController extends Controller
* Update Shipment status from action button
*/
public function updateStatus(Request $request)
{
$request->validate([
'shipment_id' => 'required|exists:shipments,id',
'status' => 'required|string'
]);
{
$request->validate([
'shipment_id' => 'required|exists:shipments,id',
'status' => 'required|string'
]);
// 1) Update shipment status
$shipment = Shipment::findOrFail($request->shipment_id);
$shipment->status = $request->status;
$shipment->save();
$shipment = Shipment::findOrFail($request->shipment_id);
$shipment->status = $request->status;
$shipment->save();
// 2) Update ALL related orders' status
foreach ($shipment->orders as $order) {
$order->status = $shipment->status; // status is string: pending, in_transit, dispatched, delivered
$order->save();
// ✅ Sync shipment status to orders ONLY after shipment exists
foreach ($shipment->orders as $order) {
// Prevent rollback or overwrite
if ($order->status === 'delivered') {
continue;
}
return redirect()->back()->with(
'success',
"Shipment status updated to {$shipment->statusLabel()} and related orders updated."
);
$order->status = $shipment->status;
$order->save();
}
return redirect()->back()->with(
'success',
"Shipment status updated to {$shipment->statusLabel()}."
);
}
/**
* Update shipment details
*/
@@ -224,5 +244,95 @@ class ShipmentController extends Controller
return view('admin.view_shipment', compact('shipment', 'dummyData'));
}
// App\Models\Shipment.php
public function orders()
{
return $this->belongsToMany(\App\Models\Order::class, 'shipment_items', 'shipment_id', 'order_id');
}
public function removeOrder(Shipment $shipment, Order $order)
{
// Remove row from pivot table shipment_items
ShipmentItem::where('shipment_id', $shipment->id)
->where('order_id', $order->id)
->delete(); // removes link shipment <-> order [web:41][web:45]
// Recalculate totals on this shipment (optional but recommended)
$orders = Order::whereIn(
'id',
ShipmentItem::where('shipment_id', $shipment->id)->pluck('order_id')
)->get();
$shipment->total_ctn = $orders->sum('ctn');
$shipment->total_qty = $orders->sum('qty');
$shipment->total_ttl_qty = $orders->sum('ttl_qty');
$shipment->total_cbm = $orders->sum('cbm');
$shipment->total_ttl_cbm = $orders->sum('ttl_cbm');
$shipment->total_kg = $orders->sum('kg');
$shipment->total_ttl_kg = $orders->sum('ttl_kg');
$shipment->total_amount = $orders->sum('ttl_amount');
$shipment->save();
// Redirect back to preview page where your blade is loaded
return redirect()
->route('admin.shipments.dummy', $shipment->id)
->with('success', 'Order removed from shipment successfully.');
}
public function addOrders(Request $request, Shipment $shipment)
{
$request->validate([
'order_ids' => 'required|array|min:1',
]);
$orders = Order::whereIn('id', $request->order_ids)->get();
foreach ($orders as $order) {
if ($order->status === 'order_placed') {
return back()->with(
'error',
"Order {$order->order_id} is not ready for shipment"
);
}
// Prevent duplicates
if (ShipmentItem::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_ttl_kg' => $order->ttl_kg,
]);
}
// Recalculate totals
$orderIds = ShipmentItem::where('shipment_id', $shipment->id)->pluck('order_id');
$allOrders = Order::whereIn('id', $orderIds)->get();
$shipment->update([
'total_ctn' => $allOrders->sum('ctn'),
'total_qty' => $allOrders->sum('qty'),
'total_ttl_qty' => $allOrders->sum('ttl_qty'),
'total_cbm' => $allOrders->sum('cbm'),
'total_ttl_cbm' => $allOrders->sum('ttl_cbm'),
'total_kg' => $allOrders->sum('kg'),
'total_ttl_kg' => $allOrders->sum('ttl_kg'),
'total_amount' => $allOrders->sum('ttl_amount'),
]);
return redirect()
->route('admin.shipments.dummy', $shipment->id)
->with('success', 'Orders added to shipment successfully.');
}
}

View File

@@ -15,7 +15,12 @@ class UserRequestController extends Controller
public function index()
{
$requests = CustomerRequest::orderBy('id', 'desc')->get();
return view('admin.requests', compact('requests'));
$pendingProfileUpdates = \App\Models\UpdateRequest::where('status', 'pending')->count();
return view('admin.requests', compact(
'requests',
'pendingProfileUpdates'
));
}
// Approve user request

View File

@@ -6,76 +6,48 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
use App\Models\User;
use Illuminate\Support\Facades\Log;
class UserAuthController extends Controller
{
public function refreshToken()
{
\Log::info('🔄 refreshToken() called');
public function refreshToken()
{
Log::info('🔄 [JWT-REFRESH] called');
try {
// Get current token
$currentToken = JWTAuth::getToken();
try {
$newToken = JWTAuth::parseToken()->refresh();
if (!$currentToken) {
\Log::warning('⚠ No token provided in refreshToken()');
return response()->json([
'success' => false,
'message' => 'Token not provided',
], 401);
}
Log::info('✅ [JWT-REFRESH] Token refreshed');
\Log::info('📥 Current Token:', ['token' => (string) $currentToken]);
return response()->json([
'success' => true,
'token' => $newToken,
]);
// Try refreshing token
$newToken = JWTAuth::refresh($currentToken);
} catch (\PHPOpenSourceSaver\JWTAuth\Exceptions\TokenExpiredException $e) {
Log::warning('⛔ [JWT-REFRESH] Refresh TTL expired');
\Log::info('✅ Token refreshed successfully', ['new_token' => $newToken]);
return response()->json([
'success' => false,
'message' => 'Refresh expired. Please login again.',
], 401);
return response()->json([
'success' => true,
'token' => $newToken,
]);
} catch (\Exception $e) {
Log::error('🔥 [JWT-REFRESH] Exception', [
'error' => $e->getMessage(),
]);
} catch (\Tymon\JWTAuth\Exceptions\TokenExpiredException $e) {
\Log::error('❌ TokenExpiredException in refreshToken()', [
'message' => $e->getMessage(),
]);
return response()->json([
'success' => false,
'message' => 'Token expired, cannot refresh.',
], 401);
} catch (\Tymon\JWTAuth\Exceptions\TokenInvalidException $e) {
\Log::error('❌ TokenInvalidException in refreshToken()', [
'message' => $e->getMessage(),
]);
return response()->json([
'success' => false,
'message' => 'Invalid token.',
], 401);
} catch (\Tymon\JWTAuth\Exceptions\JWTException $e) {
\Log::error('❌ JWTException in refreshToken()', [
'message' => $e->getMessage(),
]);
return response()->json([
'success' => false,
'message' => 'Could not refresh token.',
], 401);
} catch (\Exception $e) {
\Log::error('❌ General Exception in refreshToken()', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'success' => false,
'message' => 'Unexpected error while refreshing token.',
], 500);
}
return response()->json([
'success' => false,
'message' => 'Unable to refresh token.',
], 401);
}
}
/**
* User Login

View File

@@ -67,6 +67,12 @@ class ChatController extends Controller
'sender_id' => auth()->id(),
'sender_type' => \App\Models\User::class,
'message' => $request->message,
<<<<<<< HEAD
=======
'client_id' => $request->client_id, // ✅ ADD
'read_by_admin' => false,
'read_by_user' => true,
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
];
// Handle file upload
@@ -83,7 +89,11 @@ class ChatController extends Controller
$message->load('sender');
// Fire real-time event
<<<<<<< HEAD
broadcast(new NewChatMessage($message))->toOthers();
=======
broadcast(new NewChatMessage($message));
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
return response()->json([
'success' => true,

View File

@@ -289,6 +289,44 @@ public function invoiceDetails($invoice_id)
]);
}
public function confirmOrder($order_id)
{
$user = JWTAuth::parseToken()->authenticate();
if (! $user) {
return response()->json([
'success' => false,
'message' => 'Unauthorized'
], 401);
}
$order = $user->orders()
->where('order_id', $order_id)
->first();
if (! $order) {
return response()->json([
'success' => false,
'message' => 'Order not found'
], 404);
}
// 🚫 Only allow confirm from order_placed
if ($order->status !== 'order_placed') {
return response()->json([
'success' => false,
'message' => 'Order cannot be confirmed'
], 422);
}
$order->status = 'order_confirmed';
$order->save();
return response()->json([
'success' => true,
'message' => 'Order confirmed successfully'
]);
}

View File

@@ -11,7 +11,8 @@ use Tymon\JWTAuth\Exceptions\JWTException;
class JwtRefreshMiddleware
{
public function handle($request, Closure $next)
{
{
try {
JWTAuth::parseToken()->authenticate();
} catch (TokenExpiredException $e) {

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Imports;
use Maatwebsite\Excel\Concerns\ToCollection;
use Illuminate\Support\Collection;
class OrderItemsPreviewImport implements ToCollection
{
public array $rows = [];
public function collection(Collection $collection)
{
$header = $collection->first()->map(fn ($h) => strtolower(trim($h)))->toArray();
foreach ($collection->skip(1) as $row) {
$item = [];
foreach ($header as $i => $key) {
$item[$key] = $row[$i] ?? null;
}
if (!empty($item['description'])) {
$this->rows[] = $item;
}
}
}
}

View File

@@ -18,7 +18,7 @@ class Admin extends Authenticatable
'name', 'email', 'password', 'username',
'phone', 'emergency_phone', 'address',
'role', 'department', 'designation', 'joining_date',
'status', 'additional_info', 'type', // admin/staff indicator
'status', 'additional_info', 'type','employee_id', // admin/staff indicator
];
protected $hidden = [

View File

@@ -10,6 +10,7 @@ class ChatMessage extends Model
use HasFactory;
protected $fillable = [
<<<<<<< HEAD
'ticket_id',
'sender_id',
'sender_type', // user OR admin
@@ -17,6 +18,19 @@ class ChatMessage extends Model
'file_path',
'file_type',
];
=======
'ticket_id',
'sender_id',
'sender_type',
'message',
'file_path',
'file_type',
'read_by_admin',
'read_by_user',
'client_id',
];
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
/**
* The ticket this message belongs to.

View File

@@ -64,5 +64,25 @@ class Order extends Model
}
const STATUS_LABELS = [
'order_placed' => 'Order Placed',
'order_confirmed' => 'Order Confirmed',
'supplier_warehouse' => 'Supplier Warehouse',
'consolidate_warehouse'=> 'Consolidate Warehouse',
'export_custom' => 'Export Custom',
'international_transit'=> 'International Transit',
'arrived_india' => 'Arrived at India',
'import_custom' => 'Import Custom',
'warehouse' => 'Warehouse',
'domestic_distribution'=> 'Domestic Distribution',
'out_for_delivery' => 'Out for Delivery',
'delivered' => 'Delivered',
];
public function getStatusLabelAttribute()
{
return self::STATUS_LABELS[$this->status]
?? ucfirst(str_replace('_', ' ', $this->status));
}
}

View File

@@ -45,25 +45,6 @@ class Shipment extends Model
return $this->belongsToMany(Order::class, 'shipment_items', 'shipment_id', 'order_id');
}
// ---------------------------
// STATUS CONSTANTS
// ---------------------------
const STATUS_PENDING = 'pending';
const STATUS_IN_TRANSIT = 'in_transit';
const STATUS_DISPATCHED = 'dispatched';
const STATUS_DELIVERED = 'delivered';
public static function statusOptions()
{
return [
self::STATUS_PENDING => 'Pending',
self::STATUS_IN_TRANSIT => 'In Transit',
self::STATUS_DISPATCHED => 'Dispatched',
self::STATUS_DELIVERED => 'Delivered',
];
}
// ---------------------------
// HELPERS
// ---------------------------
@@ -73,8 +54,38 @@ class Shipment extends Model
return $this->items()->count();
}
// ---------------------------
// STATUS CONSTANTS (LOGISTICS FLOW)
// ---------------------------
const STATUS_SHIPMENT_READY = 'shipment_ready';
const STATUS_EXPORT_CUSTOM = 'export_custom';
const STATUS_INTERNATIONAL_TRANSIT= 'international_transit';
const STATUS_ARRIVED_INDIA = 'arrived_india';
const STATUS_IMPORT_CUSTOM = 'import_custom';
const STATUS_WAREHOUSE = 'warehouse';
const STATUS_DOMESTIC_DISTRIBUTION= 'domestic_distribution';
const STATUS_OUT_FOR_DELIVERY = 'out_for_delivery';
const STATUS_DELIVERED = 'delivered';
public static function statusOptions()
{
return [
self::STATUS_SHIPMENT_READY => 'Shipment Ready',
self::STATUS_EXPORT_CUSTOM => 'Export Custom',
self::STATUS_INTERNATIONAL_TRANSIT => 'International Transit',
self::STATUS_ARRIVED_INDIA => 'Arrived at India',
self::STATUS_IMPORT_CUSTOM => 'Import Custom',
self::STATUS_WAREHOUSE => 'Warehouse',
self::STATUS_DOMESTIC_DISTRIBUTION => 'Domestic Distribution',
self::STATUS_OUT_FOR_DELIVERY => 'Out for Delivery',
self::STATUS_DELIVERED => 'Delivered',
];
}
public function statusLabel()
{
return self::statusOptions()[$this->status] ?? ucfirst($this->status);
return self::statusOptions()[$this->status]
?? ucfirst(str_replace('_', ' ', $this->status));
}
}

View File

@@ -94,6 +94,19 @@ public function invoices()
return $this->hasMany(\App\Models\Invoice::class, 'customer_id', 'id');
}
// App\Models\User.php
public function invoiceInstallments()
{
return $this->hasManyThrough(
InvoiceInstallment::class,
Invoice::class,
'customer_id', // FK on invoices
'invoice_id' // FK on installments
);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;
class BroadcastServiceProvider extends ServiceProvider
{
public function boot(): void
{
Broadcast::routes([
'middleware' => ['web'],
]);
// 👇 FORCE admin guard for broadcasting
Auth::shouldUse('admin');
require base_path('routes/channels.php');
}
}

View File

@@ -6,13 +6,18 @@ use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
web: [
__DIR__.'/../routes/web.php',
__DIR__.'/../routes/channels.php',
],
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
->withMiddleware(function (Middleware $middleware) {
//
})
->withExceptions(function (Exceptions $exceptions): void {
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
})
->create();

View File

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

63
composer.lock generated
View File

@@ -2880,6 +2880,7 @@
},
{
"name": "maennchen/zipstream-php",
<<<<<<< HEAD
"version": "3.2.1",
"source": {
"type": "git",
@@ -2890,21 +2891,45 @@
"type": "zip",
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/682f1098a8fddbaf43edac2306a691c7ad508ec5",
"reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5",
=======
"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",
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"ext-zlib": "*",
<<<<<<< HEAD
"php-64bit": "^8.3"
=======
"php-64bit": "^8.2"
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
},
"require-dev": {
"brianium/paratest": "^7.7",
"ext-zip": "*",
<<<<<<< HEAD
"friendsofphp/php-cs-fixer": "^3.86",
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
"phpunit/phpunit": "^12.0",
=======
"friendsofphp/php-cs-fixer": "^3.16",
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
"phpunit/phpunit": "^11.0",
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
"vimeo/psalm": "^6.0"
},
"suggest": {
@@ -2946,7 +2971,11 @@
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
<<<<<<< HEAD
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.1"
=======
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2"
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
},
"funding": [
{
@@ -2954,7 +2983,11 @@
"type": "github"
}
],
<<<<<<< HEAD
"time": "2025-12-10T09:58:31+00:00"
=======
"time": "2025-01-27T12:07:53+00:00"
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
},
{
"name": "markbaker/complex",
@@ -4181,6 +4214,7 @@
},
{
"name": "phpoffice/phpspreadsheet",
<<<<<<< HEAD
"version": "1.30.2",
"source": {
"type": "git",
@@ -4191,6 +4225,18 @@
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/09cdde5e2f078b9a3358dd217e2c8cb4dac84be2",
"reference": "09cdde5e2f078b9a3358dd217e2c8cb4dac84be2",
=======
"version": "1.30.1",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "fa8257a579ec623473eabfe49731de5967306c4c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fa8257a579ec623473eabfe49731de5967306c4c",
"reference": "fa8257a579ec623473eabfe49731de5967306c4c",
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
"shasum": ""
},
"require": {
@@ -4213,11 +4259,19 @@
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"php": ">=7.4.0 <8.5.0",
<<<<<<< HEAD
=======
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
<<<<<<< HEAD
"doctrine/instantiator": "^1.5",
=======
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
"dompdf/dompdf": "^1.0 || ^2.0 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.3",
@@ -4264,9 +4318,12 @@
},
{
"name": "Adrien Crivelli"
<<<<<<< HEAD
},
{
"name": "Owen Leibman"
=======
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
}
],
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
@@ -4283,9 +4340,15 @@
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
<<<<<<< HEAD
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.2"
},
"time": "2026-01-11T05:58:24+00:00"
=======
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.1"
},
"time": "2025-10-26T16:01:04+00:00"
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
},
{
"name": "phpoption/phpoption",

View File

@@ -89,6 +89,8 @@ return [
'driver' => 'eloquent',
'model' => App\Models\Staff::class,
],
],

View File

@@ -89,7 +89,7 @@ return [
|
*/
'ttl' => (int) env('JWT_TTL', 60),
'ttl' => (int) env('JWT_TTL', 1440),
/*
|--------------------------------------------------------------------------
@@ -108,7 +108,7 @@ return [
|
*/
'refresh_ttl' => (int) env('JWT_REFRESH_TTL', 20160),
'refresh_ttl' => (int) env('JWT_REFRESH_TTL', 64800),
/*
|--------------------------------------------------------------------------

View File

@@ -0,0 +1,30 @@
<?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('chat_messages', function (Blueprint $table) {
$table->boolean('read_by_admin')->default(false)->after('file_type');
$table->boolean('read_by_user')->default(false)->after('read_by_admin');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('chat_messages', function (Blueprint $table) {
//
});
}
};

View File

@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('chat_messages', function (Blueprint $table) {
$table->string('client_id')
->nullable()
->after('sender_type')
->index();
});
}
public function down(): void
{
Schema::table('chat_messages', function (Blueprint $table) {
$table->dropColumn('client_id');
});
}
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -1,4 +1,9 @@
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
axios.defaults.withCredentials = true;
axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
axios.defaults.headers.common["X-CSRF-TOKEN"] = document.querySelector(
'meta[name="csrf-token"]'
).content;

View File

@@ -28,4 +28,4 @@ window.Echo = new Echo({
}
});
console.log('%c[ECHO] Initialized!', 'color: green; font-weight: bold;', window.Echo);
console.log('%c[ECHO] Initialized!', 'color: green; font-weight: bold;', window.Echo);

File diff suppressed because it is too large Load Diff

View File

@@ -92,3 +92,78 @@
</div>
@endsection
@section('scripts')
<script>
// -------------------------------
// WAIT FOR ECHO READY (DEFINE IT)
// -------------------------------
function waitForEcho(callback, retries = 40) {
if (window.Echo) {
console.log('%c[ECHO] Ready (Admin List)', 'color: green; font-weight: bold;');
callback();
return;
}
if (retries <= 0) {
console.error('[ECHO] Failed to initialize');
return;
}
setTimeout(() => waitForEcho(callback, retries - 1), 200);
}
// -------------------------------
// LISTEN FOR REALTIME MESSAGES
// -------------------------------
waitForEcho(() => {
console.log('[ADMIN LIST] Listening for new messages...');
const globalBox = document.getElementById('globalNewMessageBox');
const globalCount = document.getElementById('globalNewMessageCount');
let totalNewMessages = 0;
window.Echo.private('admin.chat')
.listen('.NewChatMessage', (event) => {
// only USER → ADMIN messages
if (event.sender_type !== 'App\\Models\\User') return;
const ticketId = event.ticket_id;
// 1) UPDATE PER-TICKET BADGE
const badge = document.getElementById(`badge-${ticketId}`);
if (badge) {
let count = parseInt(badge.innerText || 0);
badge.innerText = count + 1;
badge.classList.remove('d-none');
}
// 2) UPDATE GLOBAL NEW MESSAGE COUNTER
totalNewMessages++;
if (globalBox && globalCount) {
globalBox.classList.remove('d-none');
globalCount.innerText = totalNewMessages;
}
// 3) त्या ticket ला यादीत सर्वात वर आणा आणि स्क्रोल करा
const listContainer = document.querySelector('.tickets-list');
const ticketEl = document.querySelector(`.ticket-item[data-ticket-id="${ticketId}"]`);
if (listContainer && ticketEl) {
if (ticketEl.previousElementSibling) {
listContainer.insertBefore(ticketEl, listContainer.firstElementChild);
}
const topOffset = ticketEl.offsetTop;
window.scrollTo({
top: listContainer.offsetTop + topOffset - 20,
behavior: 'smooth'
});
}
console.log('[ADMIN LIST] Badge/global counter updated for ticket', ticketId);
});
});
</script>
@endsection

View File

@@ -197,5 +197,4 @@ waitForEcho(() => {
});
});
</script>
@endsection

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -698,11 +698,48 @@
<div class="stats-icon">
<i class="bi bi-currency-rupee"></i>
</div>
<div class="stats-value">{{ number_format($totalAmount, 2) }}</div>
<div class="stats-value">{{ number_format($totalOrderAmount, 2) }}</div>
<div class="stats-label">Total Amount Spent</div>
</div>
</div>
<!-- {{-- Total Payable --}}
<div class="stats-card amount">
<div class="stats-icon">
<i class="bi bi-wallet2"></i>
</div>
<div class="stats-value">{{ number_format($totalPayable, 2) }}</div>
<div class="stats-label">Total Payable</div>
</div>
{{-- Total Remaining --}}
<div class="stats-card marks">
<div class="stats-icon">
<i class="bi bi-exclamation-circle"></i>
</div>
<div class="stats-value">{{ number_format($totalRemaining, 2) }}</div>
<div class="stats-label">Remaining Amount</div>
</div> -->
<div class="col-md-4 animate-fade-in animation-delay-3">
<div class="stats-card marks">
<div class="stats-icon">
<i class="bi bi-hash"></i>
</div>
<div class="stats-value">{{ number_format($totalPayable, 2) }}</div>
<div class="stats-label">Total Payable</div>
</div>
</div>
<div class="col-md-4 animate-fade-in animation-delay-3">
<div class="stats-card marks">
<div class="stats-icon">
<i class="bi bi-hash"></i>
</div>
<div class="stats-value">{{ number_format($totalRemaining, 2) }}</div>
<div class="stats-label">Remaining Amount</div>
</div>
</div>
{{-- Mark Count --}}
<div class="col-md-4 animate-fade-in animation-delay-3">
<div class="stats-card marks">

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@
justify-content: space-between;
align-items: center;
border-radius: 17px 17px 0 0;
background: #fceeb8ff;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 54px;
padding: 15px 26px 10px 22px;
border-bottom: 1.4px solid #e8e2cf;
@@ -37,7 +37,7 @@
.invoice-management-title {
font-size: 1.32rem;
font-weight: 800;
color: #2451af;
color: #ffffffff;
letter-spacing: .08em;
display: flex;
align-items: center;
@@ -223,7 +223,7 @@
/* Center all table content */
.table thead tr {
background: #feebbe !important;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.table thead th:first-child {
@@ -237,7 +237,7 @@
background: transparent !important;
border: none;
font-weight: 700;
color: #343535;
color: #ffffffff;
letter-spacing: 0.02em;
font-size: 14px;
padding: 20px 15px;
@@ -258,25 +258,33 @@
/* Soft blue background for ALL table rows */
.table-striped tbody tr {
background: #f0f8ff !important;
transition: all 0.2s ease;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
background: #f0f8ff !important;
transition: all 0.15s ease;
border-radius: 6px;
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
}
.table-striped tbody tr td {
padding: 6px 10px;
font-size: 14px;
}
.table-striped tbody tr:hover {
background: #e6f3ff !important;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
transform: translateY(-0.5px);
}
.table-striped tbody tr:hover {
background: #e6f3ff !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
/* Remove striped pattern - all rows same soft blue */
.table-striped tbody tr:nth-of-type(odd),
.table-striped tbody tr:nth-of-type(even) {
background: #f0f8ff !important;
}
/* Center all table cells with proper spacing */
.table td {
padding: 18px 15px;
border: none;
@@ -291,7 +299,7 @@
font-weight: 400;
}
/* First and last cell rounded corners */
.table td:first-child {
padding-left: 30px;
font-weight: 600;
@@ -587,7 +595,7 @@
}
.date-separator {
color: #64748b;
color: #000000ff;
font-weight: 500;
font-family: 'Inter', sans-serif;
}

View File

@@ -33,6 +33,401 @@
}
}
/* --------------------------------------------------
INVOICE PREVIEW RESPONSIVE FIXES
-------------------------------------------------- */
.invoice-preview-wrapper {
width: 100%;
overflow: auto;
max-width: 100%;
box-sizing: border-box;
}
.invoice-preview-wrapper * {
box-sizing: border-box;
}
/* Override any fixed width styles that might be in popup_invoice */
#invoicePreview,
.invoice-container,
.invoice-wrapper {
max-width: 100% !important;
width: 100% !important;
}
/* Responsive table fixes for invoice */
.invoice-preview-wrapper table {
width: 100% !important;
max-width: 100% !important;
table-layout: auto !important;
}
.invoice-preview-wrapper .table-responsive {
overflow-x: auto !important;
-webkit-overflow-scrolling: touch;
}
/* Ensure all elements scale properly */
.invoice-preview-wrapper .row,
.invoice-preview-wrapper .col,
.invoice-preview-wrapper [class*="col-"] {
flex: 1 1 auto !important;
max-width: 100% !important;
}
/* Force responsive behavior for print-style elements */
@media (max-width: 1200px) {
.invoice-preview-wrapper {
font-size: 95%;
}
}
@media (max-width: 992px) {
.invoice-preview-wrapper {
font-size: 90%;
}
}
@media (max-width: 768px) {
.invoice-preview-wrapper {
font-size: 85%;
}
.invoice-preview-wrapper table th,
.invoice-preview-wrapper table td {
padding: 0.5rem !important;
}
}
@media (max-width: 576px) {
.invoice-preview-wrapper {
font-size: 80%;
}
.invoice-preview-wrapper .d-flex {
flex-direction: column !important;
}
.invoice-preview-wrapper .text-end,
.invoice-preview-wrapper .text-start {
text-align: center !important;
}
}
/* Prevent any fixed pixel widths */
.invoice-preview-wrapper [style*="width:"]:not([style*="width:100%"]):not([style*="width:auto"]) {
width: auto !important;
max-width: 100% !important;
}
.invoice-preview-wrapper [style*="min-width"] {
min-width: 0 !important;
}
/* --------------------------------------------------
COMPACT CARD DESIGN
-------------------------------------------------- */
.glass-card {
background: var(--glass-bg);
backdrop-filter: blur(20px);
border-radius: var(--border-radius);
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-strong);
position: relative;
overflow: hidden;
animation: fadeUp 0.6s ease;
margin-bottom: 1.5rem;
}
.glass-card::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(45deg,
rgba(102, 126, 234, 0.03),
rgba(118, 75, 162, 0.03) 50%,
rgba(16, 185, 129, 0.03));
pointer-events: none;
}
/* --------------------------------------------------
COMPACT CARD HEADER
-------------------------------------------------- */
.card-header-compact {
background: var(--primary-gradient);
color: #fff;
padding: 1rem 1.5rem;
border: none;
position: relative;
overflow: hidden;
}
.card-header-compact h4 {
margin: 0;
font-weight: 700;
font-size: 1.3rem;
display: flex;
align-items: center;
gap: 10px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* --------------------------------------------------
COMPACT CARD BODY
-------------------------------------------------- */
.card-body-compact {
padding: 1.5rem;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
position: relative;
z-index: 1;
}
/* --------------------------------------------------
COMPACT FORM ELEMENTS
-------------------------------------------------- */
.form-grid-compact {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.form-group-compact {
position: relative;
}
.form-label-compact {
font-weight: 600;
color: #374151;
margin-bottom: 8px;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 8px;
}
.form-control-compact, .form-select-compact {
border: 2px solid #e2e8f0;
border-radius: 8px;
padding: 10px 12px;
font-size: 0.9rem;
transition: all 0.3s ease;
background: white;
width: 100%;
}
.form-control-compact:focus, .form-select-compact:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
outline: none;
}
/* --------------------------------------------------
COMPACT BUTTONS
-------------------------------------------------- */
.btn-compact {
padding: 10px 20px;
border-radius: 8px;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease;
border: none;
display: inline-flex;
align-items: center;
gap: 6px;
text-decoration: none;
}
.btn-success-compact {
background: var(--success-gradient);
color: white;
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
.btn-success-compact:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4);
color: white;
}
.btn-primary-compact {
background: var(--primary-gradient);
color: white;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.btn-primary-compact:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
color: white;
}
.btn-danger-compact {
background: var(--danger-gradient);
color: white;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
.btn-danger-compact:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
color: white;
}
.btn-info-compact {
background: linear-gradient(135deg, #06b6d4 0%, #0ea5e9 100%);
color: white;
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.3);
}
.btn-info-compact:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(6, 182, 212, 0.4);
color: white;
}
.btn-warning-compact {
background: var(--warning-gradient);
color: white;
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
}
.btn-warning-compact:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.4);
color: white;
}
/* --------------------------------------------------
COMPACT SUMMARY CARDS
-------------------------------------------------- */
.summary-grid-compact {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.summary-card-compact {
background: white;
padding: 1rem;
border-radius: 8px;
box-shadow: var(--shadow-soft);
border-left: 4px solid;
text-align: center;
}
.summary-card-compact.total { border-left-color: #10b981; }
.summary-card-compact.paid { border-left-color: #3b82f6; }
.summary-card-compact.remaining { border-left-color: #f59e0b; }
.summary-value-compact {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.summary-label-compact {
font-size: 0.8rem;
color: #64748b;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* --------------------------------------------------
COMPACT AMOUNT BREAKDOWN
-------------------------------------------------- */
.amount-breakdown-compact {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: var(--shadow-soft);
margin-bottom: 1.5rem;
}
.breakdown-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #f1f5f9;
}
.breakdown-row:last-child {
border-bottom: none;
}
.breakdown-label {
font-weight: 600;
color: #374151;
font-size: 0.9rem;
}
.breakdown-value {
font-weight: 600;
font-size: 0.9rem;
}
/* --------------------------------------------------
COMPACT TABLE
-------------------------------------------------- */
.table-compact {
width: 100%;
border-collapse: collapse;
margin-bottom: 0;
font-size: 0.85rem;
}
.table-compact thead th {
background: var(--primary-gradient);
color: white;
padding: 0.75rem 1rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 0.8rem;
border: none;
}
.table-compact tbody tr {
background: white;
transition: all 0.2s ease;
}
.table-compact tbody tr:hover {
background: #f8fafc;
}
.table-compact tbody td {
padding: 0.75rem 1rem;
border-bottom: 1px solid #e2e8f0;
vertical-align: middle;
}
/* --------------------------------------------------
BADGE STYLES
-------------------------------------------------- */
.badge-compact {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
}
/* --------------------------------------------------
HEADER ACTION BUTTONS
-------------------------------------------------- */
.header-actions {
display: flex;
gap: 0.75rem;
align-items: center;
}
/* --------------------------------------------------
RESPONSIVE DESIGN
-------------------------------------------------- */
@media (max-width: 768px) {
.glass-card {
background: var(--glass-bg);
backdrop-filter: blur(20px);
@@ -309,16 +704,59 @@
font-size: 0.8rem;
}
}
.header-actions {
flex-direction: column;
width: 100%;
gap: 0.5rem;
margin-top: 0.5rem;
}
}
@media print {
.invoice-preview-wrapper {
max-width: 100% !important;
width: 100% !important;
overflow: visible !important;
}
.invoice-preview-wrapper * {
visibility: visible !important;
}
.glass-card {
box-shadow: none !important;
border: 1px solid #000 !important;
}
.card-header-compact {
background: #000 !important;
color: #fff !important;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
</style>
<div class="container-fluid py-3">
{{-- Invoice Preview / Overview --}}
<div class="glass-card">
<div class="card-header-compact">
<div class="card-header-compact d-flex justify-content-between align-items-center">
<h4>
<i class="fas fa-file-invoice me-2"></i>
Invoice Overview
</h4>
<div class="header-actions">
<!-- Share Invoice -->
<button class="btn-info-compact btn-compact" id="shareInvoiceBtn">
<i class="fas fa-share-alt me-2"></i>Share Invoice
</button>
<!-- Download Invoice -->
<a href="{{ route('admin.invoices.download', $invoice->id) }}"
class="btn-warning-compact btn-compact" target="_blank">
<i class="fas fa-download me-2"></i>Download Invoice
</a>
</div>
</div>
<div class="card-body-compact">
{{-- इथे popup_invoice मधूनच items table (editable) येईल --}}
@@ -459,6 +897,7 @@
</div>
</div>
<<<<<<< HEAD
@php
$totalPaid = $invoice->installments()->sum('amount');
$remaining = $invoice->final_amount_with_gst - $totalPaid;
@@ -551,6 +990,9 @@
</div>
{{-- Installment Management --}}
=======
<!-- Installment Management -->
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
<div class="glass-card">
<div class="card-header-compact d-flex justify-content-between align-items-center">
<h4 class="mb-0">
@@ -643,12 +1085,7 @@
<h6 class="fw-bold mb-2 text-dark">
<i class="fas fa-history me-2"></i>Installment History
</h6>
<div id="noInstallmentsMsg"
class="{{ $invoice->installments->count() ? 'd-none' : '' }} text-center text-muted fw-bold py-4">
No installments found. Click Add Installment to create one.
</div>
@if($invoice->installments->count() > 0)
<div class="table-responsive">
<table class="table-compact">
<thead>
@@ -691,6 +1128,11 @@
</tbody>
</table>
</div>
@else
<div id="noInstallmentsMsg" class="text-center text-muted fw-bold py-4">
No installments found. Click "Add Installment" to create one.
</div>
@endif
</div>
</div>
</div>
@@ -717,6 +1159,7 @@ document.addEventListener("DOMContentLoaded", function () {
});
};
<<<<<<< HEAD
if (submitForm && submitBtn) {
submitForm.addEventListener("submit", function (e) {
e.preventDefault();
@@ -825,12 +1268,131 @@ document.addEventListener("DOMContentLoaded", function () {
const id = row.getAttribute("data-id");
fetch("{{ url('admin/installment') }}/" + id, {
=======
if (submitForm) {
submitForm.addEventListener("submit", function (e) {
e.preventDefault();
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Processing...';
submitBtn.disabled = true;
fetch("{{ route('admin.invoice.installment.store', $invoice->id) }}", {
method: "POST",
headers: {
"X-CSRF-TOKEN": submitForm.querySelector("input[name=_token]").value,
"Accept": "application/json"
},
body: new FormData(submitForm)
})
.then(res => res.json())
.then(data => {
submitBtn.innerHTML = '<i class="fas fa-paper-plane me-2"></i>Submit Installment';
submitBtn.disabled = false;
if (data.status === "error") {
alert(data.message);
return;
}
const table = document.querySelector("#installmentTable");
const noInstallmentsMsg = document.getElementById("noInstallmentsMsg");
if (noInstallmentsMsg) {
noInstallmentsMsg.classList.add("d-none");
}
if (!table) {
// Create table if it doesn't exist
const tableHTML = `
<div class="table-responsive">
<table class="table-compact">
<thead>
<tr>
<th>#</th>
<th>Date</th>
<th>Payment Method</th>
<th>Reference No</th>
<th>Amount</th>
<th>Action</th>
</tr>
</thead>
<tbody id="installmentTable">
</tbody>
</table>
</div>
`;
const parent = submitForm.closest('.card-body-compact');
parent.insertAdjacentHTML('beforeend', tableHTML);
}
const newTable = document.querySelector("#installmentTable");
const index = newTable.rows.length + 1;
newTable.insertAdjacentHTML("beforeend", `
<tr data-id="${data.installment.id}">
<td class="fw-bold text-muted">${index}</td>
<td>${data.installment.installment_date}</td>
<td>
<span class="badge-compact bg-primary bg-opacity-10 text-primary">
${data.installment.payment_method.toUpperCase()}
</span>
</td>
<td>${data.installment.reference_no || '-'}</td>
<td class="fw-bold text-success">${formatINR(data.installment.amount)}</td>
<td>
<button class="btn-danger-compact btn-compact btn-sm deleteInstallment">
<i class="fas fa-trash me-1"></i>Delete
</button>
</td>
</tr>
`);
// Update all displayed values
if (document.getElementById("paidAmount")) document.getElementById("paidAmount").textContent = formatINR(data.totalPaid);
if (document.getElementById("remainingAmount")) document.getElementById("remainingAmount").textContent = formatINR(data.remaining);
// Update summary cards
const paidCard = document.querySelector(".summary-card-compact.paid .summary-value-compact");
if (paidCard) paidCard.textContent = formatINR(data.totalPaid);
const remainingCard = document.querySelector(".summary-card-compact.remaining .summary-value-compact");
if (remainingCard) remainingCard.textContent = formatINR(data.remaining);
submitForm.reset();
// If fully paid, disable/add display logic
if (data.isCompleted) {
toggleBtn?.remove();
formBox.classList.add("d-none");
}
alert(data.message);
})
.catch(() => {
submitBtn.innerHTML = '<i class="fas fa-paper-plane me-2"></i>Submit Installment';
submitBtn.disabled = false;
alert("Something went wrong. Please try again.");
});
});
}
// Delete Installment
document.addEventListener("click", function (e) {
if (!e.target.classList.contains("deleteInstallment")) return;
if (!confirm("Are you sure you want to delete this installment?")) return;
const row = e.target.closest("tr");
const id = row.getAttribute("data-id");
fetch("{{ url('/admin/installment') }}/" + id, {
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
method: "DELETE",
headers: {
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').getAttribute("content"),
"Accept": "application/json",
},
})
<<<<<<< HEAD
.then(res => res.json())
.then(data => {
if (data.status === "success") {
@@ -858,6 +1420,49 @@ document.addEventListener("DOMContentLoaded", function () {
const remainingCard = document.querySelector(".summary-card-compact.remaining .summary-value-compact");
if (remainingCard) remainingCard.textContent = "" + formatINR(data.remaining);
=======
.then(res => res.json())
.then(data => {
if (data.status === "success") {
row.style.opacity = "0";
setTimeout(() => {
row.remove();
// Update row numbers
const rows = document.querySelectorAll("#installmentTable tr");
rows.forEach((row, index) => {
row.querySelector("td:first-child").textContent = index + 1;
});
// Show no installments message if empty
if (rows.length === 0) {
const noInstallmentsMsg = document.getElementById("noInstallmentsMsg");
if (noInstallmentsMsg) {
noInstallmentsMsg.classList.remove("d-none");
}
}
}, 300);
// Update all displayed values
if (document.getElementById("paidAmount")) document.getElementById("paidAmount").textContent = formatINR(data.totalPaid);
if (document.getElementById("remainingAmount")) document.getElementById("remainingAmount").textContent = formatINR(data.remaining);
// Update summary cards
const paidCard = document.querySelector(".summary-card-compact.paid .summary-value-compact");
if (paidCard) paidCard.textContent = formatINR(data.totalPaid);
const remainingCard = document.querySelector(".summary-card-compact.remaining .summary-value-compact");
if (remainingCard) remainingCard.textContent = formatINR(data.remaining);
// Show add installment button if there's remaining amount
if (data.remaining > 0 && !document.getElementById("toggleInstallmentForm")) {
const header = document.querySelector(".glass-card .card-header-compact");
header.innerHTML += `
<button id="toggleInstallmentForm" class="btn-primary-compact btn-compact">
<i class="fas fa-plus-circle me-2"></i>Add Installment
</button>
`;
}
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
if (data.isZero) {
document.getElementById("noInstallmentsMsg")?.classList.remove("d-none");
@@ -874,4 +1479,8 @@ document.addEventListener("DOMContentLoaded", function () {
});
});
</script>
<<<<<<< HEAD
@endsection
=======
@endsection
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Admin Panel</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
@@ -199,7 +199,7 @@
background: #fff;
padding: 10px 18px !important;
position: relative;
height: 48px;
height: 65px;
width: 100%;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
@@ -219,6 +219,32 @@
font-size: 1.06rem;
font-weight: 500;
}
/* ================================
HEADER NOTIFICATION BADGE FIX
================================ */
/* Target ONLY badge inside bell icon */
header .bi-bell {
position: relative;
}
/* Override broken global badge styles */
header .bi-bell .badge {
width: 22px !important;
height: 22px !important;
min-width: 22px !important;
padding: 0 !important;
font-size: 12px !important;
line-height: 22px !important;
border-radius: 50% !important;
display: inline-flex !important;
align-items: center;
justify-content: center;
animation: none !important;
box-shadow: 0 0 0 2px #ffffff;
}
</style>
</head>
@@ -295,12 +321,12 @@
</a>
@endcan
{{-- Profile Update Requests --}}
<!-- {{-- Profile Update Requests --}}
@can('request.update_profile')
<a href="{{ route('admin.profile.requests') }}" class="{{ request()->routeIs('admin.profile.requests') ? 'active' : '' }}">
<i class="bi bi-person-lines-fill"></i> Profile Update Requests
</a>
@endcan
<a href="{{ route('admin.profile.requests') }}">
<i class="bi bi-person-lines-fill"></i> Profile Update Requests
</a>
@endcan -->
{{-- Staff --}}
@can('staff.view')
@@ -371,7 +397,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
@vite(['resources/js/app.js'])
@yield('scripts')
@yield('scripts') <!-- REQUIRED FOR CHAT TO WORK -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const headerToggle = document.getElementById('headerToggle');

File diff suppressed because it is too large Load Diff

View File

@@ -11,60 +11,64 @@
</style>
</head>
<body>
<h3>Orders Report</h3>
@if(!empty($filters))
<p>
@if($filters['search']) Search: <strong>{{ $filters['search'] }}</strong> @endif
@if($filters['status']) &nbsp; | Status: <strong>{{ ucfirst($filters['status']) }}</strong> @endif
@if($filters['shipment']) &nbsp; | Shipment: <strong>{{ ucfirst($filters['shipment']) }}</strong> @endif
</p>
@endif
<h3>Orders Report</h3>
<table>
<thead>
@if(!empty($filters))
<p>
@if(!empty($filters['search'])) Search: <strong>{{ $filters['search'] }}</strong> @endif
@if(!empty($filters['status'])) | Status: <strong>{{ ucfirst($filters['status']) }}</strong> @endif
@if(!empty($filters['shipment'])) | Shipment: <strong>{{ ucfirst($filters['shipment']) }}</strong> @endif
</p>
@endif
<table>
<thead>
<tr>
<th>Order ID</th>
<th>Shipment ID</th>
<th>Customer ID</th>
<th>Company</th>
<th>Origin</th>
<th>Destination</th>
<th>Order Date</th>
<th>Invoice No</th>
<th>Invoice Date</th>
<th>Amount</th>
<th>Amount + GST</th>
<th>Invoice Status</th>
<th>Shipment Status</th>
</tr>
</thead>
<tbody>
@forelse($orders as $order)
@php
$mark = $order->markList;
$invoice = $order->invoice;
$shipment = $order->shipments->first();
@endphp
<tr>
<th>Order ID</th>
<th>Shipment ID</th>
<th>Customer ID</th>
<th>Company</th>
<th>Origin</th>
<th>Destination</th>
<th>Order Date</th>
<th>Invoice No</th>
<th>Invoice Date</th>
<th>Amount</th>
<th>Amount + GST</th>
<th>Invoice Status</th>
<th>Shipment Status</th>
<td>{{ $order->order_id }}</td>
<td>{{ $shipment?->shipment_id ?? '-' }}</td>
<td>{{ $mark?->customer_id ?? '-' }}</td>
<td>{{ $mark?->company_name ?? '-' }}</td>
<td>{{ $mark?->origin ?? $order->origin ?? '-' }}</td>
<td>{{ $mark?->destination ?? $order->destination ?? '-' }}</td>
<td>{{ $order->created_at?->format('d-m-Y') ?? '-' }}</td>
<td>{{ $invoice?->invoice_number ?? '-' }}</td>
<td>{{ $invoice?->invoice_date ? \Carbon\Carbon::parse($invoice->invoice_date)->format('d-m-Y') : '-' }}</td>
<td>{{ $invoice?->final_amount !== null ? number_format($invoice->final_amount, 2) : '-' }}</td>
<td>{{ $invoice?->final_amount_with_gst !== null ? number_format($invoice->final_amount_with_gst, 2) : '-' }}</td>
<td>{{ ucfirst($invoice?->status ?? 'Pending') }}</td>
<td>{{ ucfirst(str_replace('_',' ', $shipment?->status ?? 'Pending')) }}</td>
</tr>
</thead>
<tbody>
@forelse($orders as $order)
@php
$mark = $order->markList ?? null;
$invoice = $order->invoice ?? null;
$shipment = $order->shipments->first() ?? null;
@endphp
<tr>
<td>{{ $order->order_id }}</td>
<td>{{ $shipment->shipment_id ?? '-' }}</td>
<td>{{ $mark->customer_id ?? '-' }}</td>
<td>{{ $mark->company_name ?? '-' }}</td>
<td>{{ $mark->origin ?? $order->origin ?? '-' }}</td>
<td>{{ $mark->destination ?? $order->destination ?? '-' }}</td>
<td>{{ $order->created_at ? $order->created_at->format('d-m-Y') : '-' }}</td>
<td>{{ $invoice->invoice_number ?? '-' }}</td>
<td>{{ $invoice?->invoice_date ? \Carbon\Carbon::parse($invoice->invoice_date)->format('d-m-Y') : '-' }}</td>
<td>{{ $invoice?->final_amount ? number_format($invoice->final_amount, 2) : '-' }}</td>
<td>{{ $invoice?->final_amount_with_gst ? number_format($invoice->final_amount_with_gst, 2) : '-' }}</td>
<td>{{ $invoice->status ? ucfirst($invoice->status) : 'Pending' }}</td>
<td>{{ $shipment?->status ? ucfirst(str_replace('_',' ',$shipment->status)) : 'Pending' }}</td>
</tr>
@empty
<tr><td colspan="13" style="text-align:center">No orders found</td></tr>
@endforelse
</tbody>
</table>
@empty
<tr>
<td colspan="13" style="text-align:center">No orders found</td>
</tr>
@endforelse
</tbody>
</table>
</body>
</html>

View File

@@ -13,47 +13,54 @@
<div class="d-flex justify-content-between align-items-start">
<div>
<h4 class="fw-bold mb-0">Order Details</h4>
@php
$status = strtolower($order->status ?? '');
@endphp
<small class="text-muted">Detailed view of this shipment order</small>
</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>
@if($status === 'pending')
<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>
@endif
@endcan
<a href="{{ route('admin.dashboard') }}" class="btn-close"></a>
</div>
<!-- {{-- ACTION BUTTONS --}}
<div class="mt-3 d-flex gap-2">
<div class="mt-3 d-flex gap-2">-->
<div class="mt-3 d-flex gap-2">
{{-- Edit Order --}}
@if($status === 'pending')
<button class="btn btn-edit-order"
onclick="document.getElementById('editOrderForm').style.display='block'">
<i class="fas fa-edit me-2"></i>Edit Order
</button>
@endif
{{-- EDIT ORDER --}} -->
<!-- @if($order->status === 'pending')
<button class="btn btn-edit-order" onclick="document.getElementById('editOrderForm').style.display='block'">
<i class="fas fa-edit me-2"></i>Edit Order
</button>
@else
<button class="btn btn-edit-order" disabled><i class="fas fa-edit me-2"></i>Edit Order</button>
@endif -->
{{-- Delete Order --}}
@if($status === 'pending')
<form action="{{ route('admin.orders.destroy', $order->id) }}"
method="POST"
onsubmit="return confirm('Delete this entire order?')">
@csrf
@method('DELETE')
<button class="btn btn-delete-order">
<i class="fas fa-trash-alt me-2"></i>Delete Order
</button>
</form>
@endif
</div>
<!-- {{-- DELETE ORDER --}}
@if($order->status === 'pending')
<form action="{{ route('admin.orders.destroy', $order->id) }}"
method="POST"
onsubmit="return confirm('Delete this entire order?')">
@csrf
@method('DELETE')
<button class="btn btn-delete-order">
<i class="fas fa-trash-alt me-2"></i>Delete Order
</button>
</form>
@endif -->
<!-- </div> -->
<hr>
@@ -190,32 +197,33 @@
<td>{{ $item->ttl_kg }}</td>
<td>{{ $item->shop_no }}</td>
<td class="d-flex justify-content-center gap-2">
<td class="d-flex justify-content-center gap-2">
@if($status === 'pending')
{{-- EDIT BUTTON --}}
@can('order.edit')
<button type="button"
class="btn btn-sm btn-edit-item"
data-bs-toggle="modal"
data-bs-target="#editItemModal{{ $item->id }}">
<i class="fas fa-edit"></i>
</button>
@endcan
{{-- EDIT BUTTON --}}
@can('order.edit')
<button
type="button"
class="btn btn-sm btn-edit-item"
data-bs-toggle="modal"
data-bs-target="#editItemModal{{ $item->id }}">
<i class="fas fa-edit"></i>
</button>
@endcan
{{-- DELETE BUTTON --}}
@can('order.delete')
<form action="{{ route('admin.orders.deleteItem', $item->id) }}"
method="POST"
onsubmit="return confirm('Delete this item?')">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-delete-item">
<i class="fas fa-trash"></i>
</button>
</form>
@endcan
@endif
</td>
@can('order.delete')
{{-- DELETE BUTTON --}}
<form action="{{ route('admin.orders.deleteItem', $item->id) }}"
method="POST"
onsubmit="return confirm('Delete this item?')">
@csrf
@method('DELETE')
<button class="btn btn-sm btn-delete-item">
<i class="fas fa-trash"></i>
</button>
</form>
@endcan
</td>
</tr>
@endforeach
@@ -617,7 +625,7 @@ function fillFormFromDeleted(item) {
box-shadow: 0 4px 15px 0 rgba(102, 126, 234, 0.3);
position: relative;
overflow: hidden;
margin-right: -800px;
margin-right: -650px;
}
.btn-add-item:hover {

View File

@@ -4,177 +4,167 @@
<meta charset="UTF-8">
<title>{{ $invoice->invoice_number }}</title>
<style>
body {
font-family: 'Segoe UI', 'Roboto', Arial, sans-serif;
background: #F7FBFC;
color: #1A222B;
margin: 0;
padding: 0;
font-size: 15px;
}
.container {
max-width: 850px;
margin: 24px auto 0 auto;
background: #fff;
border-radius: 13px;
box-shadow: 0 2px 14px rgba(40,105,160,0.08);
padding: 35px 32px 18px 32px;
}
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
border-bottom: 2px solid #E6EBF0;
padding-bottom: 13px;
}
.logo-company {
display: flex;
align-items: flex-start;
}
.logo {
height: 50px;
margin-right: 13px;
}
.company-details {
margin-top: 0;
font-size: 15px;
}
.company-title {
font-size: 21px;
font-weight: bold;
margin-bottom: 2px;
}
.company-sub {
font-size: 16px;
margin-bottom: 8px;
font-weight: 500;
}
.invoice-details {
text-align: right;
min-width: 220px;
}
.invoice-title {
font-weight: bold;
font-size: 23px;
letter-spacing: 0.5px;
margin-bottom: 2px;
}
.paid-label {
margin-top: 8px;
margin-bottom: 2px;
}
.paid-tag {
background: #23BF47;
color: #fff;
font-weight: bold;
border-radius: 8px;
padding: 4px 16px 4px 22px;
font-size: 17px;
display: inline-block;
position: relative;
}
.paid-tag:before {
content: '';
position: absolute;
left: 7px;
top: 7px;
width: 10px;
height: 10px;
background: #fff;
border-radius: 50%;
}
.paid-date {
font-size: 14px;
color: #23BF47;
margin-top: 2px;
}
body {
font-family: 'Segoe UI', 'Roboto', Arial, sans-serif;
background: #F7FBFC;
color: #1A222B;
margin: 0;
padding: 0;
font-size: 15px;
}
.bill-section {
background: #F3F7FB;
border-radius: 11px;
padding: 20px 18px 13px 18px;
margin: 28px 0 16px 0;
box-shadow: 0 0px 0px #0000;
}
.bill-title {
font-size: 17px;
font-weight: bold;
color: #23355D;
margin-bottom: 4px;
letter-spacing: 0.3px;
}
.bill-details {
font-size: 15px;
line-height: 1.6;
}
.container {
max-width: 850px;
margin: 24px auto 0 auto;
background: #fff;
border-radius: 13px;
box-shadow: 0 2px 14px rgba(40,105,160,0.08);
padding: 35px 32px 18px 32px;
}
/* ================= HEADER FIX ================= */
.header {
width: 100%;
overflow: hidden; /* clears floats */
border-bottom: 2px solid #E6EBF0;
padding-bottom: 13px;
}
/* LEFT SIDE */
.logo-company {
float: left;
width: 65%;
}
/* RIGHT SIDE */
.invoice-details {
float: right;
width: 35%;
text-align: right;
}
/* Logo */
.logo {
height: 50px;
margin-bottom: 6px;
}
/* Company text */
.company-details {
font-size: 15px;
line-height: 1.55;
}
.company-title {
font-size: 21px;
font-weight: bold;
margin-bottom: 4px;
}
/* Invoice */
.invoice-title {
font-weight: bold;
font-size: 23px;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
/* Paid / Status */
.paid-tag {
background: #23BF47;
color: #fff;
font-weight: bold;
border-radius: 8px;
padding: 4px 16px 4px 22px;
font-size: 17px;
display: inline-block;
position: relative;
}
.paid-tag:before {
content: '';
position: absolute;
left: 7px;
top: 7px;
width: 10px;
height: 10px;
background: #fff;
border-radius: 50%;
}
.paid-date {
font-size: 14px;
color: #23BF47;
margin-top: 3px;
}
/* ================= REST ================= */
.bill-section {
background: #F3F7FB;
border-radius: 11px;
padding: 20px 18px 13px 18px;
margin: 28px 0 16px 0;
}
.bill-title {
font-size: 17px;
font-weight: bold;
color: #23355D;
margin-bottom: 4px;
}
.bill-details {
font-size: 15px;
line-height: 1.6;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 9px;
margin-bottom: 13px;
}
th {
background: #F6F7F9;
padding: 10px 0;
font-size: 15px;
color: #6781A6;
font-weight: bold;
text-align: left;
}
td {
padding: 7px 0;
font-size: 15px;
color: #222;
}
tbody tr:not(:last-child) td {
border-bottom: 1px solid #E6EBF0;
}
.footer {
border-top: 1.2px solid #E6EBF0;
margin-top: 25px;
padding-top: 12px;
font-size: 16px;
color: #888;
text-align: center;
}
.gst-row td {
color: #23BF47;
font-weight: 500;
}
.total-row td {
font-weight: bold;
font-size: 17px;
border-top: 2px solid #E6EBF0;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 9px;
margin-bottom: 13px;
background: #fff;
border-radius: 10px;
overflow: hidden;
}
th {
background: #F6F7F9;
padding: 10px 0;
font-size: 15px;
color: #6781A6;
font-weight: bold;
border: none;
text-align: left;
}
td {
padding: 7px 0;
color: #222;
font-size: 15px;
border: none;
text-align: left;
}
tbody tr:not(:last-child) td {
border-bottom: 1px solid #E6EBF0;
}
tbody tr:last-child td {
border-bottom: none;
}
.totals-row td {
font-weight: bold;
color: #23355D;
}
.gst-row td {
font-weight: 500;
color: #23BF47;
}
.total-row td {
font-weight: bold;
font-size: 17px;
color: #222;
}
.payment-info {
margin-top: 24px;
margin-bottom: 9px;
font-size: 15px;
}
.ref-number {
font-size: 14px;
color: #6781A6;
margin-bottom: 8px;
margin-top: 2px;
}
.footer {
border-top: 1.2px solid #E6EBF0;
margin-top: 25px;
padding-top: 12px;
font-size: 16px;
color: #888;
text-align: center;
}
.footer strong {
color: #222;
font-weight: 500;
}
</style>
</head>
<body>
@@ -241,18 +231,47 @@
<td>{{ number_format($item->ttl_amount, 0) }}</td>
</tr>
@endforeach
{{-- SUBTOTAL --}}
<tr class="totals-row">
<td colspan="3" style="text-align:right;">Subtotal:</td>
<td>{{ number_format($invoice->subtotal, 0) }}</td>
</tr>
<tr class="gst-row">
<td colspan="3" style="text-align:right;">GST ({{ $invoice->gst_percent }}%):</td>
<td>{{ number_format($invoice->gst_amount, 0) }}</td>
<td colspan="3" style="text-align:right;">total</td>
<td style="text-align:right;">
{{ number_format($invoice->final_amount, 2) }}
</td>
</tr>
{{-- TAX --}}
@if($invoice->tax_type === 'gst' && $invoice->gst_amount > 0)
<tr class="gst-row">
<td colspan="3" style="text-align:right;">
GST ({{ $invoice->gst_percent }}%)
</td>
<td style="text-align:right;">
{{ number_format($invoice->gst_amount, 2) }}
</td>
</tr>
@elseif($invoice->tax_type === 'igst' && $invoice->gst_amount > 0)
<tr class="gst-row">
<td colspan="3" style="text-align:right;">
IGST ({{ $invoice->gst_percent }}%)
</td>
<td style="text-align:right;">
{{ number_format($invoice->gst_amount, 2) }}
</td>
</tr>
@endif
{{-- TOTAL --}}
<tr class="total-row">
<td colspan="3" style="text-align:right;">Total:</td>
<td>{{ number_format($invoice->final_amount_with_gst, 0) }}</td>
<td colspan="3" style="text-align:right;">Total Amount</td>
<td style="text-align:right;">
{{ number_format($invoice->final_amount_with_gst, 2) }}
</td>
</tr>
</tbody>
</table>
<!-- Payment Info & Reference -->

View File

@@ -9,6 +9,7 @@
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
/* ALL YOUR EXISTING CSS STAYS HERE - EXCEPT GST TOTALS SECTION REMOVED */
:root {
--primary: #2c3e50;
--secondary: #3498db;
@@ -69,6 +70,9 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
height: 100%;
display: flex;
align-items: center;
gap: 1rem;
}
.id-box:hover {
@@ -83,11 +87,10 @@
.id-box-secondary {
border-left: 4px solid var(--success);
}
.id-icon {
width: 36px;
height: 36px;
border-radius: 50%;
width: 48px;
height: 48px;
border-radius: var(--border-radius);
display: flex;
align-items: center;
justify-content: center;
@@ -96,15 +99,14 @@
}
.id-icon-primary {
background: rgba(52, 152, 219, 0.1);
color: var(--secondary);
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
color: white;
}
.id-icon-secondary {
background: rgba(39, 174, 96, 0.1);
color: var(--success);
background: linear-gradient(135deg, #27ae60 0%, #219653 100%);
color: white;
}
.id-label {
font-size: 0.75rem;
color: #6c757d;
@@ -119,6 +121,8 @@
font-weight: 700;
color: var(--primary);
margin-bottom: 0;
word-break: break-word;
line-height: 1.3;
}
.date-container {
@@ -173,8 +177,7 @@
.date-connector {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
gap: 4px;
}
.date-connector i {
@@ -183,6 +186,7 @@
border-radius: 50%;
color: var(--secondary);
border: 2px solid #e9ecef;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.card {
@@ -220,17 +224,19 @@
.table tbody tr:hover {
background-color: rgba(52, 152, 219, 0.03);
}
.summary-card {
background: var(--light);
border-left: 4px solid var(--secondary);
/* Simplified Summary Section */
.summary-container {
margin: 2rem 0;
}
.summary-header {
background: var(--primary);
color: #ffffff;
.amount-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
}
.text-primary {
color: var(--primary) !important;
}
@@ -276,6 +282,66 @@
.id-box {
margin-bottom: 1rem;
}
.date-status-row {
justify-content: flex-start;
margin-top: 0.5rem;
}
.compact-header .col-md-6.text-end {
text-align: left !important;
}
.date-badge {
min-width: 120px;
}
.summary-container {
padding: 0 1rem;
}
}
@media (max-width: 576px) {
.date-status-row {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.date-separator {
display: none;
}
.date-badge {
width: 100%;
}
.id-box {
flex-direction: column;
text-align: center;
gap: 0.75rem;
}
.id-icon {
width: 40px;
height: 40px;
font-size: 1rem;
}
.table-responsive {
overflow-x: auto;
}
}
@media print {
body {
background-color: white;
}
.invoice-container {
box-shadow: none;
border: 1px solid #ddd;
}
}
</style>
</head>
@@ -623,11 +689,12 @@
url: "{{ asset($invoice->pdf_path) }}"
};
if (navigator.share) {
navigator.share(shareData).catch(() => {});
} else {
navigator.clipboard.writeText(shareData.url);
alert("Link copied! Sharing not supported on this browser.");
if (navigator.share) {
navigator.share(shareData).catch(() => {});
} else {
navigator.clipboard.writeText(shareData.url);
alert("Link copied! Sharing not supported on this browser.");
}
}
}
</script>

View File

@@ -5,107 +5,207 @@
@section('content')
<div class="container-fluid px-0">
@php
$perPage = 5;
$currentPage = request()->get('page', 1);
$currentPage = max(1, (int)$currentPage);
$total = $requests->count();
$totalPages = ceil($total / $perPage);
$currentItems = $requests->slice(($currentPage - 1) * $perPage, $perPage);
@endphp
@php
$perPage = 5;
$currentPage = request()->get('page', 1);
$currentPage = max(1, (int)$currentPage);
$total = $requests->count();
$currentItems = $requests->slice(($currentPage - 1) * $perPage, $perPage);
@endphp
<style>
.old-value { color: #6b7280; font-weight: 600; }
.new-value { color: #111827; font-weight: 700; }
.changed { background: #fef3c7; padding: 6px; border-radius: 6px; }
.box { padding: 10px 14px; border-radius: 8px; background: #f8fafc; margin-bottom: 10px; }
.diff-label { font-size: 13px; font-weight: 700; }
.actions { display: flex; gap: 10px; }
</style>
<style>
/* ===== Card Wrapper ===== */
.request-card {
background: #ffffff;
border-radius: 14px;
padding: 18px;
margin-bottom: 18px;
box-shadow: 0 6px 20px rgba(0,0,0,0.05);
}
<h4 class="fw-bold my-3">Profile Update Requests ({{ $total }})</h4>
/* ===== Header Row ===== */
.request-header {
display: grid;
grid-template-columns: 60px 1.5fr 1fr 1.2fr 1fr;
gap: 14px;
align-items: center;
border-bottom: 1px solid #e5e7eb;
padding-bottom: 12px;
margin-bottom: 14px;
}
<div class="card mb-4 shadow-sm">
<div class="card-body pb-1">
.request-header strong {
font-size: 14px;
}
<div class="table-responsive custom-table-wrapper">
<table class="table align-middle mb-0 custom-table">
<thead>
<tr>
<th>#</th>
<th>User</th>
<th>Requested Changes</th>
<th>Status</th>
<th>Requested At</th>
<th>Actions</th>
</tr>
</thead>
/* ===== Badges ===== */
.badge {
padding: 6px 14px;
font-size: 12px;
border-radius: 999px;
font-weight: 700;
}
<tbody>
@foreach($currentItems as $index => $req)
@php
$user = $req->user;
// FIX: Convert string to array
$newData = is_array($req->data) ? $req->data : json_decode($req->data, true);
@endphp
.badge-pending {
background: #fff7ed;
color: #c2410c;
border: 1px solid #fed7aa;
}
<tr>
<td><strong>{{ ($currentPage - 1) * $perPage + $index + 1 }}</strong></td>
.badge-approved {
background: #ecfdf5;
color: #047857;
border: 1px solid #a7f3d0;
}
<td>
<strong>{{ $user->customer_name }}</strong><br>
<small>{{ $user->email }}</small><br>
<small>ID: {{ $user->customer_id }}</small>
</td>
.badge-rejected {
background: #fef2f2;
color: #b91c1c;
border: 1px solid #fecaca;
}
<td>
@foreach($newData as $key => $newValue)
@php
$oldValue = $user->$key ?? '—';
$changed = $oldValue != $newValue;
@endphp
/* ===== Action Buttons ===== */
.actions {
display: flex;
gap: 10px;
}
<div class="box {{ $changed ? 'changed' : '' }}">
<span class="diff-label">{{ ucfirst(str_replace('_',' ', $key)) }}:</span><br>
<span class="old-value">Old: {{ $oldValue }}</span><br>
<span class="new-value">New: {{ $newValue ?? '—' }}</span>
</div>
@endforeach
</td>
.actions .btn {
padding: 6px 14px;
font-size: 13px;
border-radius: 999px;
font-weight: 600;
}
<td>
@if($req->status == 'pending')
<span class="badge badge-pending">Pending</span>
@elseif($req->status == 'approved')
<span class="badge badge-approved">Approved</span>
@else
<span class="badge badge-rejected">Rejected</span>
@endif
</td>
/* ===== Detail Grid ===== */
.detail-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
margin-top: 12px;
}
<td>{{ $req->created_at->format('d M Y, h:i A') }}</td>
/* ===== Detail Box ===== */
.detail-box {
background: #f8fafc;
border-radius: 10px;
padding: 12px 14px;
border: 1px solid #e2e8f0;
}
<td class="actions">
@if($req->status == 'pending')
<a href="{{ route('admin.profile.approve', $req->id) }}" class="btn btn-success btn-sm">
<i class="bi bi-check-circle"></i> Approve
</a>
.detail-box.changed {
background: linear-gradient(145deg, #fff7ed, #ffedd5);
border-left: 4px solid #f59e0b;
}
<a href="{{ route('admin.profile.reject', $req->id) }}" class="btn btn-danger btn-sm">
<i class="bi bi-x-circle"></i> Reject
</a>
@else
<span class="text-muted">Completed</span>
@endif
</td>
.detail-label {
font-size: 12px;
font-weight: 700;
color: #334155;
}
</tr>
@endforeach
</tbody>
</table>
</div>
.old {
font-size: 12px;
color: #64748b;
}
.new {
font-size: 14px;
font-weight: 700;
color: #020617;
}
/* ===== Responsive ===== */
@media (max-width: 992px) {
.request-header {
grid-template-columns: 1fr;
}
.detail-grid {
grid-template-columns: 1fr;
}
}
</style>
<h4 class="fw-bold my-3">Profile Update Requests ({{ $total }})</h4>
@foreach($currentItems as $index => $req)
@php
$user = $req->user;
$newData = is_array($req->data) ? $req->data : json_decode($req->data, true);
@endphp
<div class="request-card">
<!-- HEADER -->
<div class="request-header">
<strong>#{{ ($currentPage - 1) * $perPage + $index + 1 }}</strong>
<div>
<strong>{{ $user->customer_name }}</strong><br>
<small>{{ $user->email }}</small><br>
<small>ID: {{ $user->customer_id }}</small>
</div>
<div>
@if($req->status == 'pending')
<span class="badge badge-pending">Pending</span>
@elseif($req->status == 'approved')
<span class="badge badge-approved">Approved</span>
@else
<span class="badge badge-rejected">Rejected</span>
@endif
</div>
<div>{{ $req->created_at->format('d M Y, h:i A') }}</div>
<div class="actions">
@if($req->status == 'pending')
<a href="{{ route('admin.profile.approve', $req->id) }}" class="btn btn-success btn-sm">
Approve
</a>
<a href="{{ route('admin.profile.reject', $req->id) }}" class="btn btn-danger btn-sm">
Reject
</a>
@else
<span class="text-muted">Completed</span>
@endif
</div>
</div>
<!-- DETAILS ROW 1 -->
<div class="detail-grid">
@foreach(['customer_name','company_name','email'] as $field)
@php
$old = $user->$field ?? '—';
$new = $newData[$field] ?? $old;
@endphp
<div class="detail-box {{ $old != $new ? 'changed' : '' }}">
<div class="detail-label">{{ ucfirst(str_replace('_',' ', $field)) }}</div>
<div class="old">Old: {{ $old }}</div>
<div class="new">New: {{ $new }}</div>
</div>
@endforeach
</div>
<!-- DETAILS ROW 2 -->
<div class="detail-grid">
@foreach(['mobile_no','address','pincode'] as $field)
@php
$old = $user->$field ?? '—';
$new = $newData[$field] ?? $old;
@endphp
<div class="detail-box {{ $old != $new ? 'changed' : '' }}">
<div class="detail-label">{{ ucfirst(str_replace('_',' ', $field)) }}</div>
<div class="old">Old: {{ $old }}</div>
<div class="new">New: {{ $new }}</div>
</div>
@endforeach
</div>
</div>
@endforeach
</div>
@endsection

View File

@@ -912,10 +912,6 @@
</label>
<select class="filter-control" id="companyFilter">
<option value="" selected>All Companies</option>
<option value="ABC Corporation">ABC Corporation</option>
<option value="XYZ Enterprises">XYZ Enterprises</option>
<option value="Global Traders">Global Traders</option>
<option value="Tech Solutions Ltd">Tech Solutions Ltd</option>
</select>
</div>
<div class="filter-group">
@@ -1101,6 +1097,7 @@
document.addEventListener('DOMContentLoaded', function() {
// Initialize statistics and pagination
populateCompanyFilter(allReports);
updateStatistics();
renderTable();
updatePaginationControls();
@@ -1403,5 +1400,33 @@
// Initial filter application
filterTable();
});
function populateCompanyFilter(reports) {
const companySelect = document.getElementById('companyFilter');
if (!companySelect) return;
// Collect unique company names
const companies = new Set();
reports.forEach(r => {
if (r.company_name && r.company_name.trim() !== '') {
companies.add(r.company_name.trim());
}
});
// Sort alphabetically
const sortedCompanies = Array.from(companies).sort();
// Remove existing options except "All Companies"
companySelect.innerHTML = '<option value="">All Companies</option>';
// Append options
sortedCompanies.forEach(company => {
const option = document.createElement('option');
option.value = company;
option.textContent = company;
companySelect.appendChild(option);
});
}
</script>
@endsection

View File

@@ -22,7 +22,7 @@
.custom-table tbody tr:hover { background-color: #fffbea; transform: scale(1.01); box-shadow: 0 2px 6px rgba(0,0,0,0.05); }
.priority-badge {
display: inline-flex; align-items: center; font-size: 13.5px; padding: 6px 16px; border-radius: 12px; font-weight: 600;
box-shadow: 0 1px 2px 0 rgba(130,130,130,0.15); width: 90px; min-height: 28px; justify-content: center;
box-shadow: 0 1px 2px 0 rgba(230, 206, 206, 0.15); width: 90px; min-height: 28px; justify-content: center;
color: #fff; margin: 2px 0; transition: transform 0.2s ease-in-out;
}
.priority-badge:hover { transform: scale(1.08); }
@@ -30,8 +30,8 @@
.priority-medium { background: linear-gradient(135deg, #ffe390, #f5b041); }
.priority-low { background: linear-gradient(135deg, #b8f0c2, #1d8660); }
.custom-table thead th {
text-align: center; font-weight: 700; color: #000; padding: 14px; font-size: 17px; letter-spacing: 0.5px;
border-bottom: 2px solid #bfbfbf; background-color: #fde4b3;
text-align: center; font-weight: 700; color: #ffffffff; padding: 14px; font-size: 17px; letter-spacing: 0.5px;
border-bottom: 2px solid #bfbfbf; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);;
}
.custom-table thead tr:first-child th:first-child { border-top-left-radius: 12px; }
.custom-table thead tr:first-child th:last-child { border-top-right-radius: 12px; }
@@ -307,13 +307,58 @@
justify-content: center;
}
}
/* ==============================================
PROFILE UPDATE REQUEST BUTTON BADGE FIX
============================================== */
/* Ensure button is positioning context */
a.btn.btn-primary.position-relative {
position: relative;
margin-right: 10px;
}
/* Fix badge inside Profile Update Requests button */
a.btn.btn-primary.position-relative .badge {
width: 30px !important;
height: 30px !important;
min-width: 30px !important;
padding: 0 !important;
font-size: 14px !important;
line-height: 30px !important;
border-radius: 50% !important;
display: inline-flex !important;
align-items: center;
justify-content: center;
animation: none !important;
box-shadow: 0 0 0 2px #ffffff;
}
.custom-table th,
.custom-table td {
text-align: center;
vertical-align: middle;
}
</style>
<!-- Counts -->
<div class="d-flex justify-content-between align-items-center mb-2 mt-3">
<h4 class="fw-bold mb-0">User Requests (Total: {{ $total }})</h4>
</div>
<h4 class="fw-bold mb-0">User Requests (Total: {{ $total }})</h4>
@can('request.update_profile')
<a href="{{ route('admin.profile.requests') }}" class="btn btn-primary position-relative">
<i class="bi bi-person-lines-fill me-1"></i>
Profile Update Requests
@if($pendingProfileUpdates > 0)
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
{{ $pendingProfileUpdates }}
</span>
@endif
</a>
@endcan
</div>
<!-- Search + Table -->
<div class="card mb-4 shadow-sm">

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,65 +1,896 @@
@extends('admin.layouts.app')
@section('page-title', 'Account Dashboard')
@section('page-title', 'Staff Management 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; }
:root {
--primary: #4361ee;
--primary-dark: #3a56d4;
--secondary: #f72585;
--success: #4cc9f0;
--warning: #f8961e;
--danger: #e63946;
--light: #f8f9fa;
--dark: #212529;
--gray: #6c757d;
--border: #e2e8f0;
--card-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--hover-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
/* Search Bar - Similar to Shipment */
.search-staff-bar {
display: flex;
align-items: center;
gap: 15px;
padding: 20px;
background: var(--gradient-primary);
border-radius: 16px;
box-shadow: var(--card-shadow);
flex-wrap: wrap;
margin-bottom: 30px;
color: white;
position: relative;
overflow: hidden;
}
.search-staff-bar::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255,255,255,0.1);
z-index: 0;
}
.search-staff-bar > * {
position: relative;
z-index: 1;
}
.search-staff-bar input,
.search-staff-bar select {
padding: 12px 16px;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 10px;
flex: 1;
min-width: 150px;
background: rgba(255,255,255,0.9);
font-weight: 500;
transition: all 0.3s ease;
color: var(--dark);
}
.search-staff-bar input:focus,
.search-staff-bar select:focus {
background: white;
box-shadow: 0 0 0 3px rgba(255,255,255,0.3);
outline: none;
}
.btn-add-staff {
background: rgba(255,255,255,0.2);
backdrop-filter: blur(10px);
color: white;
border: 1px solid rgba(255,255,255,0.3);
padding: 12px 24px;
border-radius: 10px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
white-space: nowrap;
font-weight: 600;
text-decoration: none;
}
.btn-add-staff:hover {
background: rgba(255,255,255,0.3);
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.search-icon {
font-size: 20px;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
}
.user-icon {
font-size: 18px;
}
@media (max-width: 768px) {
.search-staff-bar {
flex-direction: column;
align-items: stretch;
}
.search-staff-bar input,
.search-staff-bar select {
width: 100%;
}
}
/* Card Styles - Same as Shipment */
.card {
border: none;
border-radius: 16px;
box-shadow: var(--card-shadow);
transition: all 0.3s ease;
overflow: hidden;
}
.card:hover {
transform: translateY(-5px);
box-shadow: var(--hover-shadow);
}
.card-header {
background: var(--gradient-primary);
color: white;
border: none;
padding: 20px 25px;
border-radius: 16px 16px 0 0 !important;
}
.card-header h5 {
margin: 0;
font-weight: 700;
display: flex;
align-items: center;
gap: 10px;
}
/* Table Styles - Similar to Shipment */
.table-responsive {
border-radius: 0 0 16px 16px;
overflow-x: auto;
}
.table {
margin: 0;
border-collapse: separate;
border-spacing: 0;
width: 100%;
padding: 0;
}
.table thead th {
background: #f8f9fa;
border: none;
padding: 16px 12px;
font-weight: 700;
color: var(--dark);
text-align: left;
vertical-align: middle;
border-bottom: 2px solid var(--border);
position: relative;
}
.table tbody tr {
transition: all 0.3s ease;
}
.table tbody tr:hover {
background-color: #f8f9ff;
transform: scale(1.01);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.table tbody td {
padding: 14px 12px;
text-align: left;
vertical-align: middle;
border-bottom: 1px solid var(--border);
font-weight: 500;
}
.table tbody tr:last-child td {
border-bottom: none;
}
/* Status Badges - Similar Style */
.badge {
padding: 6px 12px !important;
border-radius: 20px !important;
font-weight: 600 !important;
font-size: 12px !important;
border: 2px solid transparent !important;
min-width: 80px !important;
text-align: center !important;
display: inline-block !important;
line-height: 1.2 !important;
}
.badge-active {
background: linear-gradient(135deg, #d1fae5, #a7f3d0) !important;
color: #065f46 !important;
border-color: #10b981 !important;
}
.badge-inactive {
background: linear-gradient(135deg, #fecaca, #fca5a5) !important;
color: #991b1b !important;
border-color: #ef4444 !important;
}
.badge-pending {
background: linear-gradient(135deg, #fef3c7, #fde68a) !important;
color: #92400e !important;
border-color: #f59e0b !important;
}
/* Employee ID Badge - Similar to Shipment ID */
.employee-id-badge {
font-family: 'Courier New', monospace;
background: rgba(67, 97, 238, 0.1);
padding: 4px 8px;
border-radius: 6px;
font-size: 0.85rem;
color: var(--primary);
border: 1px solid rgba(67, 97, 238, 0.2);
display: inline-block;
}
/* Action Buttons - Similar Style */
.action-buttons {
display: flex;
gap: 8px;
}
.btn-action {
padding: 6px 12px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 5px;
border: none;
cursor: pointer;
}
.btn-edit {
background: linear-gradient(135deg, #4cc9f0, #4361ee);
color: white;
}
.btn-edit:hover {
background: linear-gradient(135deg, #38bdf8, #3a56d4);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(76, 201, 240, 0.3);
}
.btn-delete {
background: linear-gradient(135deg, #f87171, #ef4444);
color: white;
}
.btn-delete:hover {
background: linear-gradient(135deg, #ef4444, #dc2626);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
/* Success Message - Similar Style */
.alert-success {
background: linear-gradient(135deg, #e6ffed, #d1f7e5);
border: 1px solid #b6f0c6;
border-left: 4px solid var(--success);
color: #0f5132;
padding: 1rem 1.25rem;
border-radius: 10px;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.alert-success:before {
content: '✓';
background: var(--success);
color: white;
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: var(--gray);
}
.empty-state:before {
content: '👤';
font-size: 3rem;
display: block;
margin-bottom: 1rem;
opacity: 0.5;
}
/* Role Badges */
.role-badge {
padding: 4px 8px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
background: rgba(67, 97, 238, 0.1);
color: var(--primary);
border: 1px solid rgba(67, 97, 238, 0.2);
}
/* Stats Cards - Similar to Shipment Totals */
.stats-cards {
display: flex;
gap: 20px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.stat-card {
flex: 1;
min-width: 200px;
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: var(--card-shadow);
display: flex;
align-items: center;
gap: 15px;
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-3px);
box-shadow: var(--hover-shadow);
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.stat-icon.total {
background: linear-gradient(135deg, #e6f3ff, #c2d9ff);
color: var(--primary);
}
.stat-icon.active {
background: linear-gradient(135deg, #d1fae5, #a7f3d0);
color: #10b981;
}
.stat-content h3 {
font-size: 1.8rem;
font-weight: 700;
margin: 0;
color: var(--dark);
}
.stat-content p {
color: var(--gray);
margin: 4px 0 0 0;
font-size: 0.875rem;
}
/* Pagination - Same as Shipment */
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15px;
padding: 12px 25px;
border-top: 1px solid #eef3fb;
}
.pagination-info {
font-size: 13px;
color: #9ba5bb;
font-weight: 600;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 8px;
}
.pagination-btn {
background: #fff;
border: 1px solid #e3eaf6;
color: #1a2951;
padding: 8px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 40px;
height: 32px;
}
.pagination-btn:hover:not(:disabled) {
background: #1a2951;
color: white;
border-color: #1a2951;
}
.pagination-btn:disabled {
background: #f8fafc;
color: #cbd5e0;
border-color: #e2e8f0;
cursor: not-allowed;
opacity: 0.6;
}
.pagination-page-btn {
background: #fff;
border: 1px solid #e3eaf6;
color: #1a2951;
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
min-width: 36px;
text-align: center;
}
.pagination-page-btn:hover {
background: #1a2951;
color: white;
border-color: #1a2951;
}
.pagination-page-btn.active {
background: #1a2951;
color: white;
border-color: #1a2951;
}
.pagination-pages {
display: flex;
gap: 4px;
align-items: center;
}
.pagination-ellipsis {
color: #9ba5bb;
font-size: 13px;
padding: 0 4px;
}
@media (max-width: 768px) {
.stats-cards {
flex-direction: column;
}
.stat-card {
min-width: 100%;
}
.pagination-container {
flex-direction: column;
gap: 10px;
align-items: stretch;
}
.pagination-controls {
justify-content: center;
}
}
</style>
<div class="top-bar">
<h2>Staff</h2>
<a href="{{ route('admin.staff.create') }}" class="btn primary">Add Staff</a>
<div class="container-fluid py-4">
@if(session('success'))
<div class="alert-success">
{{ session('success') }}
</div>
@endif
{{-- Stats Cards --}}
<div class="stats-cards">
<div class="stat-card">
<div class="stat-icon total">
👥
</div>
<div class="stat-content">
<h3>{{ $staff->count() }}</h3>
<p>Total Staff</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon active">
</div>
<div class="stat-content">
<h3>{{ $staff->where('status', 'active')->count() }}</h3>
<p>Active Staff</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, #f3e8ff, #e9d5ff); color: #8b5cf6;">
👑
</div>
<div class="stat-content">
<h3>{{ $staff->unique('role')->count() }}</h3>
<p>Unique Roles</p>
</div>
</div>
</div>
{{-- Search Bar --}}
<div class="search-staff-bar">
<span class="search-icon">🔍</span>
<input type="text" id="searchInput" placeholder="Search by name, email, or employee ID...">
<div class="status-filter-container">
<select id="statusFilter" class="status-filter-select">
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="pending">Pending</option>
</select>
</div>
<select id="roleFilter">
<option value="all">All Roles</option>
@foreach($staff->unique('role')->pluck('role') as $role)
@if($role)
<option value="{{ $role }}">{{ ucfirst($role) }}</option>
@endif
@endforeach
</select>
<a href="{{ route('admin.staff.create') }}" class="btn-add-staff">
<span class="user-icon"></span>
Add Staff
</a>
</div>
{{-- Staff Table --}}
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-people me-2"></i> Staff Management</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<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 id="staffTableBody">
@php
$totalStaff = count($staff);
@endphp
@forelse($staff as $s)
<tr class="staff-row" data-status="{{ $s->status }}" data-role="{{ $s->role ?? '' }}">
<td class="fw-bold">{{ $totalStaff - $loop->index }}</td>
<td>
<span class="employee-id-badge">{{ $s->employee_id }}</span>
</td>
<td>
<div class="d-flex align-items-center gap-2">
<div style="width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, #667eea, #764ba2); display: flex; align-items: center; justify-content: center; color: white; font-weight: bold;">
{{ strtoupper(substr($s->name, 0, 1)) }}
</div>
<span class="fw-medium">{{ $s->name }}</span>
</div>
</td>
<td>{{ $s->email }}</td>
<td>{{ $s->phone ?? '-' }}</td>
<td>
@if($s->role)
<span class="role-badge">{{ $s->role }}</span>
@else
<span class="text-muted">-</span>
@endif
</td>
<td>
<span class="badge badge-{{ $s->status }}">
{{ ucfirst($s->status) }}
</span>
</td>
<td>
<div class="action-buttons">
<a href="{{ route('admin.staff.edit', $s->id) }}" class="btn-action btn-edit">
<i class="bi bi-pencil"></i>
Edit
</a>
<form action="{{ route('admin.staff.destroy', $s->id) }}" method="POST" style="display:inline" onsubmit="return confirm('Are you sure you want to delete this staff member?')">
@csrf
@method('DELETE')
<button type="submit" class="btn-action btn-delete">
<i class="bi bi-trash"></i>
Delete
</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="8" class="text-center py-5 text-muted">
<div class="empty-state">
No staff members found
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{-- Pagination --}}
<div class="pagination-container">
<div class="pagination-info" id="pageInfo">
Showing 1 to {{ $staff->count() }} of {{ $staff->count() }} entries
</div>
<div class="pagination-controls">
<button class="pagination-btn" id="prevPageBtn" title="Previous page" disabled>
<i class="bi bi-chevron-left"></i>
</button>
<div class="pagination-pages" id="paginationPages">
<button class="pagination-page-btn active">1</button>
</div>
<button class="pagination-btn" id="nextPageBtn" title="Next page" disabled>
<i class="bi bi-chevron-right"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="card">
@if(session('success'))
<div style="padding:.5rem; background:#e6ffed; border:1px solid #b6f0c6; margin-bottom:1rem;">{{ session('success') }}</div>
@endif
<script>
// Pagination state
let currentPage = 1;
const itemsPerPage = 10;
let allStaff = @json($staff);
let filteredStaff = [...allStaff];
<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
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
renderTable();
updatePaginationControls();
// Bind pagination events
document.getElementById('prevPageBtn').addEventListener('click', goToPreviousPage);
document.getElementById('nextPageBtn').addEventListener('click', goToNextPage);
// Filter functionality
const statusFilter = document.getElementById('statusFilter');
const searchInput = document.getElementById('searchInput');
const roleFilter = document.getElementById('roleFilter');
function filterStaff() {
const selectedStatus = statusFilter.value;
const searchTerm = searchInput.value.toLowerCase();
const selectedRole = roleFilter.value;
filteredStaff = allStaff.filter(staff => {
let include = true;
// Status filter
if (selectedStatus !== 'all' && staff.status !== selectedStatus) {
include = false;
}
// Role filter
if (selectedRole !== 'all') {
const staffRole = staff.role || '';
if (staffRole.toLowerCase() !== selectedRole.toLowerCase()) {
include = false;
}
}
// Search filter
if (searchTerm) {
const matchesSearch =
staff.name.toLowerCase().includes(searchTerm) ||
staff.email.toLowerCase().includes(searchTerm) ||
(staff.employee_id && staff.employee_id.toLowerCase().includes(searchTerm)) ||
(staff.phone && staff.phone.toLowerCase().includes(searchTerm));
if (!matchesSearch) include = false;
}
return include;
});
currentPage = 1;
renderTable();
updatePaginationControls();
}
// Event listeners for filters
statusFilter.addEventListener('change', filterStaff);
searchInput.addEventListener('input', filterStaff);
roleFilter.addEventListener('change', filterStaff);
// Initialize filter
filterStaff();
});
// Pagination Functions
function goToPreviousPage() {
if (currentPage > 1) {
currentPage--;
renderTable();
updatePaginationControls();
}
}
function goToNextPage() {
const totalPages = Math.ceil(filteredStaff.length / itemsPerPage);
if (currentPage < totalPages) {
currentPage++;
renderTable();
updatePaginationControls();
}
}
function updatePaginationControls() {
const totalPages = Math.ceil(filteredStaff.length / itemsPerPage);
const prevBtn = document.getElementById('prevPageBtn');
const nextBtn = document.getElementById('nextPageBtn');
const pageInfo = document.getElementById('pageInfo');
const paginationPages = document.getElementById('paginationPages');
prevBtn.disabled = currentPage === 1;
nextBtn.disabled = currentPage === totalPages || totalPages === 0;
// Update page info text
const startIndex = (currentPage - 1) * itemsPerPage + 1;
const endIndex = Math.min(currentPage * itemsPerPage, filteredStaff.length);
pageInfo.textContent = `Showing ${startIndex} to ${endIndex} of ${filteredStaff.length} entries`;
// Generate page numbers
paginationPages.innerHTML = '';
if (totalPages <= 7) {
for (let i = 1; i <= totalPages; i++) {
addPageButton(i, paginationPages);
}
} else {
addPageButton(1, paginationPages);
if (currentPage > 3) {
paginationPages.innerHTML += '<span class="pagination-ellipsis">...</span>';
}
const start = Math.max(2, currentPage - 1);
const end = Math.min(totalPages - 1, currentPage + 1);
for (let i = start; i <= end; i++) {
addPageButton(i, paginationPages);
}
if (currentPage < totalPages - 2) {
paginationPages.innerHTML += '<span class="pagination-ellipsis">...</span>';
}
addPageButton(totalPages, paginationPages);
}
}
function addPageButton(pageNumber, container) {
const button = document.createElement('button');
button.className = 'pagination-page-btn';
if (pageNumber === currentPage) {
button.classList.add('active');
}
button.textContent = pageNumber;
button.addEventListener('click', () => {
currentPage = pageNumber;
renderTable();
updatePaginationControls();
});
container.appendChild(button);
}
// Render Table
function renderTable() {
const tbody = document.getElementById('staffTableBody');
if (filteredStaff.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-5 text-muted">
<div class="empty-state">
No staff members found matching your criteria
</div>
</td>
</tr>
`;
return;
}
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedItems = filteredStaff.slice(startIndex, endIndex);
const sortedItems = [...paginatedItems].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
tbody.innerHTML = '';
sortedItems.forEach((staff, index) => {
const displayIndex = filteredStaff.length - (startIndex + index);
const row = document.createElement('tr');
row.className = 'staff-row';
row.setAttribute('data-status', staff.status);
row.setAttribute('data-role', staff.role || '');
row.innerHTML = `
<td class="fw-bold">${displayIndex}</td>
<td>
<span class="employee-id-badge">${staff.employee_id}</span>
</td>
<td>
<div class="d-flex align-items-center gap-2">
<div style="width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, #667eea, #764ba2); display: flex; align-items: center; justify-content: center; color: white; font-weight: bold;">
${staff.name ? staff.name.charAt(0).toUpperCase() : '?'}
</div>
<span class="fw-medium">${staff.name}</span>
</div>
</td>
<td>${staff.email}</td>
<td>${staff.phone || '-'}</td>
<td>
${staff.role ? `<span class="role-badge">${staff.role}</span>` : '<span class="text-muted">-</span>'}
</td>
<td>
<span class="badge badge-${staff.status}">
${staff.status.charAt(0).toUpperCase() + staff.status.slice(1)}
</span>
</td>
<td>
<div class="action-buttons">
<a href="/admin/staff/${staff.id}/edit" class="btn-action btn-edit">
<i class="bi bi-pencil"></i>
Edit
</a>
<form action="/admin/staff/${staff.id}" method="POST" style="display:inline" onsubmit="return confirm('Are you sure you want to delete this staff member?')">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<input type="hidden" name="_method" value="DELETE">
<button type="submit" class="btn-action btn-delete">
<i class="bi bi-trash"></i>
Delete
</button>
</form>
</div>
</td>
`;
tbody.appendChild(row);
});
}
</script>
@endsection

File diff suppressed because it is too large Load Diff

View File

@@ -12,12 +12,15 @@ use App\Http\Controllers\MarkListController;
use App\Http\Controllers\User\UserOrderController;
use App\Http\Controllers\User\UserProfileController;
use App\Http\Controllers\User\ChatController;
<<<<<<< HEAD
Broadcast::routes(['middleware' => ['auth:api']]);
=======
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
//user send request
Route::post('/signup-request', [RequestController::class, 'usersignup']);
@@ -26,9 +29,10 @@ Route::post('/signup-request', [RequestController::class, 'usersignup']);
Route::post('/user/login', [UserAuthController::class, 'login']);
Route::post('/auth/refresh', [UserAuthController::class, 'refreshToken']);
Route::middleware(['auth:api'])->group(function () {
//Route::post('/user/refresh', [UserAuthController::class, 'refreshToken']);
Route::post('/user/logout', [UserAuthController::class, 'logout']);
@@ -43,9 +47,11 @@ Route::middleware(['auth:api'])->group(function () {
Route::get('/user/order/{order_id}/shipment', [UserOrderController::class, 'orderShipment']);
Route::get('/user/order/{order_id}/invoice', [UserOrderController::class, 'orderInvoice']);
Route::get('/user/order/{order_id}/track', [UserOrderController::class, 'trackOrder']);
Route::get('/user/invoice/{invoice_id}/details', [UserOrderController::class, 'invoiceDetails']);
Route::post('/user/orders/{order_id}/confirm', [UserOrderController::class, 'confirmOrder']);
// Invoice List
Route::get('/user/invoice/{invoice_id}/details', [UserOrderController::class, 'invoiceDetails']);
Route::get('/user/invoices', [UserOrderController::class, 'allInvoices']);
Route::get('/user/invoice/{invoice_id}/installments', [UserOrderController::class, 'invoiceInstallmentsById']);
@@ -58,7 +64,11 @@ Route::middleware(['auth:api'])->group(function () {
// Route::post('/user/profile/update', [UserProfileController::class, 'updateProfile']);
<<<<<<< HEAD
// ===========================
=======
// ===========================
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
// CHAT SUPPORT ROUTES
// ===========================
Route::get('/user/chat/start', [ChatController::class, 'startChat']);
@@ -69,6 +79,7 @@ Route::middleware(['auth:api'])->group(function () {
});
<<<<<<< HEAD
Route::post('/broadcasting/auth', function (Request $request) {
if (!auth()->check()) {
return response()->json(['message' => 'Unauthenticated'], 401);
@@ -76,3 +87,30 @@ Route::post('/broadcasting/auth', function (Request $request) {
return Broadcast::auth($request);
})->middleware('auth:api');
=======
Route::post('/broadcasting/auth', function (Request $request) {
$user = auth('api')->user(); // JWT user (Flutter)
if (! $user) {
\Log::warning('BROADCAST AUTH FAILED - NO USER');
return response()->json(['message' => 'Unauthorized'], 401);
}
\Log::info('BROADCAST AUTH OK', [
'user_id' => $user->id,
'channel' => $request->channel_name,
]);
return Broadcast::auth(
$request->setUserResolver(fn () => $user)
);
});
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043

View File

@@ -39,4 +39,4 @@ Broadcast::channel('ticket.{ticketId}', function ($user, $ticketId) {
\Log::warning('❌ Authorization failed');
return false;
});
});

View File

@@ -12,8 +12,9 @@ use App\Http\Controllers\Admin\AdminAccountController;
use App\Http\Controllers\Admin\AdminReportController;
use App\Http\Controllers\Admin\AdminStaffController;
use App\Http\Controllers\Admin\AdminChatController;
use Illuminate\Support\Facades\Broadcast;
use App\Http\Controllers\ContainerController;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Http\Request;
// ---------------------------
@@ -62,13 +63,18 @@ Route::post('/broadcasting/auth', function (\Illuminate\Http\Request $request) {
// ADMIN LOGIN ROUTES
// ---------------------------
// login routes (public)
Route::prefix('admin')->group(function () {
Route::prefix('admin')->middleware('web')->group(function () {
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', function () {
return redirect()->route('admin.login');
})->name('login');
//Broadcast::routes([
//'middleware' => ['web', 'auth:admin'],
@@ -79,7 +85,7 @@ Broadcast::routes(['middleware' => ['web']]);
// PROTECTED ADMIN ROUTES (session protected)
// ==========================================
Route::prefix('admin')
->middleware('auth:admin')
->middleware(['web', 'auth:admin'])
->group(function () {
// Dashboard
@@ -192,7 +198,15 @@ Route::prefix('admin')
Route::get('/orders/view/{id}', [AdminOrderController::class, 'popup'])
->name('admin.orders.popup');
Route::post('/admin/orders/temp/add', [AdminOrderController::class, 'addTempItem'])
->name('admin.orders.temp.add');
// Route::get('/orders/{id}', [AdminOrderController::class, 'view'])
// ->name('admin.orders.view');
Route::get('/orders/{order:order_id}/see', [AdminOrderController::class, 'see'])
->name('admin.orders.see');
// ---------------------------
// ORDERS (FIXED ROUTES)
// ---------------------------
@@ -211,6 +225,12 @@ Route::prefix('admin')
// Delete full order
Route::delete('/orders/{id}/delete', [AdminOrderController::class, 'destroy'])
->name('admin.orders.destroy');
Route::post('/orders/upload-excel-preview',
[AdminOrderController::class, 'uploadExcelPreview']
)->name('admin.orders.upload.excel.preview');
// ---------------------------
// SHIPMENTS (FIXED ROUTES)
@@ -239,11 +259,21 @@ Route::prefix('admin')
// Shipment Delete
Route::delete('/shipments/{id}', [ShipmentController::class, 'destroy'])
->name('admin.shipments.destroy');
Route::get('/shipment/dummy/{id}', [ShipmentController::class, 'dummy'])
->name('admin.shipments.dummy');
// web.php
Route::delete('/shipments/{shipment}/orders/{order}',
[ShipmentController::class, 'removeOrder']
)->name('admin.shipments.removeOrder');
Route::post('/shipments/{shipment}/add-orders',
[ShipmentController::class, 'addOrders']
)->name('admin.shipments.addOrders');
Route::get('/shipment/dummy/{id}', [ShipmentController::class, 'dummy'])
->name('admin.shipments.dummy');
// ---------------------------
// INVOICES
// ---------------------------
@@ -265,7 +295,11 @@ Route::prefix('admin')
Route::post('/invoices/{id}/installment', [AdminInvoiceController::class, 'storeInstallment'])
->name('admin.invoice.installment.store');
Route::get(
'/admin/invoices/{id}/download',
[AdminInvoiceController::class, 'downloadInvoice']
)->name('admin.invoices.download');
Route::delete('/installment/{id}', [AdminInvoiceController::class, 'deleteInstallment'])
->name('admin.invoice.installment.delete');
@@ -276,8 +310,8 @@ Route::prefix('admin')
//Add New Invoice
Route::get('/admin/invoices/create', [InvoiceController::class, 'create'])->name('admin.invoices.create');
// //Add New Invoice
Route::get('/admin/invoices/create', [InvoiceController::class, 'create'])->name('admin.invoices.create');
// ---------------------------
@@ -316,7 +350,7 @@ Route::prefix('admin')
// ADMIN ACCOUNT (AJAX) ROUTES
// ==========================================
Route::prefix('admin/account')
->middleware('auth:admin')
->middleware(['web', 'auth:admin'])
->name('admin.account.')
->group(function () {
@@ -375,7 +409,7 @@ Route::prefix('admin')
->name('admin.orders.download.excel');
Route::prefix('admin/account')->middleware('auth:admin')->name('admin.account.')->group(function () {
Route::prefix('admin/account')->middleware(['web', 'auth:admin'])->name('admin.account.')->group(function () {
Route::post('/toggle-payment', [AdminAccountController::class, 'togglePayment'])->name('toggle');
});
@@ -383,7 +417,7 @@ Route::prefix('admin')
//Edit Button Route
//---------------------------
// protected admin routes
Route::middleware(['auth:admin'])
Route::middleware(['web', 'auth:admin'])
->prefix('admin')
->name('admin.')
->group(function () {