Compare commits
41 Commits
main
...
cb24cf575b
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
cb24cf575b | ||
|
|
9423c79c80 | ||
|
|
8a958b9c48 | ||
|
|
338425535e | ||
|
|
f7856a6755 | ||
|
|
8f95091673 | ||
|
|
7362ef6bdc | ||
|
|
e872b83ea3 | ||
|
|
6ccf2cf84e | ||
|
|
4637f0b189 | ||
|
|
952dd7eddd | ||
|
|
451be1a533 | ||
|
|
cd9a786ef4 | ||
|
|
a6dd919d3f | ||
|
|
e0a8a5c69c | ||
|
|
7fa03688aa | ||
|
|
1885d3beef | ||
|
|
72a81fa111 | ||
|
|
044bfe5563 | ||
|
|
8ca8f05b93 | ||
|
|
2741129740 | ||
|
|
1bce2be826 | ||
|
|
ea2532efc8 | ||
|
|
ccce02f43e | ||
|
|
cdb6cab57d | ||
|
|
3941b06355 | ||
|
|
d2730e78f6 | ||
|
|
80c6e42e0c | ||
|
|
e455c271c4 | ||
|
|
48f7ab82ff | ||
|
|
c4097ecbde | ||
|
|
8a0d122e2c | ||
|
|
7ef28e06ae | ||
|
|
752f5ee873 | ||
|
|
84bf42f992 | ||
|
|
fc9a401a8c | ||
|
|
3590e8f873 | ||
|
|
f6fb304b7a | ||
|
|
6b41a447bb | ||
|
|
5dc9fc7db4 | ||
|
|
1aad6b231e |
101
app/Events/NewChatMessage.php
Normal file
101
app/Events/NewChatMessage.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\ChatMessage;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class NewChatMessage implements ShouldBroadcastNow
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public $message;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(ChatMessage $message)
|
||||
{
|
||||
// Also load sender polymorphic relationship
|
||||
$message->load('sender');
|
||||
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* The channel the event should broadcast on.
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return [
|
||||
new PrivateChannel('ticket.' . $this->message->ticket_id),
|
||||
new PrivateChannel('admin.chat') // 👈 ADD THIS
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data sent to frontend (Blade + Flutter)
|
||||
*/
|
||||
public function broadcastWith()
|
||||
{
|
||||
\Log::info('APP_URL USED IN EVENT', [
|
||||
'url' => config('app.url'),
|
||||
]);
|
||||
|
||||
\Log::info("DEBUG: NewChatMessage broadcasting on channel ticket.".$this->message->ticket_id);
|
||||
|
||||
\Log::info("EVENT BROADCAST FIRED", [
|
||||
'channel' => 'ticket.'.$this->message->ticket_id,
|
||||
'sender_type' => $this->message->sender_type,
|
||||
'sender_id' => $this->message->sender_id,
|
||||
]);
|
||||
|
||||
|
||||
return [
|
||||
'id' => $this->message->id,
|
||||
'ticket_id' => $this->message->ticket_id,
|
||||
'sender_id' => $this->message->sender_id,
|
||||
'sender_type' => $this->message->sender_type,
|
||||
'message' => $this->message->message,
|
||||
'client_id' => $this->message->client_id,
|
||||
|
||||
// ✅ relative path only
|
||||
'file_path' => $this->message->file_path ?? null,
|
||||
'file_type' => $this->message->file_type ?? null,
|
||||
|
||||
'sender' => [
|
||||
'id' => $this->message->sender->id,
|
||||
'name' => $this->getSenderName(),
|
||||
'is_admin' => $this->message->sender_type === \App\Models\Admin::class,
|
||||
],
|
||||
|
||||
'created_at' => $this->message->created_at->toDateTimeString(),
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to extract sender name
|
||||
*/
|
||||
private function getSenderName()
|
||||
{
|
||||
$sender = $this->message->sender;
|
||||
|
||||
// User has customer_name (in your app)
|
||||
if ($this->message->sender_type === \App\Models\User::class) {
|
||||
return $sender->customer_name ?? $sender->name ?? "User";
|
||||
}
|
||||
|
||||
// Admin model has ->name
|
||||
return $sender->name ?? "Admin";
|
||||
}
|
||||
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'NewChatMessage';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
$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')),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
105
app/Http/Controllers/Admin/AdminChatController.php
Normal file
105
app/Http/Controllers/Admin/AdminChatController.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\SupportTicket;
|
||||
use App\Models\ChatMessage;
|
||||
use App\Events\NewChatMessage;
|
||||
|
||||
class AdminChatController extends Controller
|
||||
{
|
||||
/**
|
||||
* 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'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Page 2: Open chat window for a specific user
|
||||
*/
|
||||
public function openChat($ticketId)
|
||||
{
|
||||
$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
|
||||
*/
|
||||
public function sendMessage(Request $request, $ticketId)
|
||||
{
|
||||
$request->validate([
|
||||
'message' => 'nullable|string',
|
||||
'file' => 'nullable|file|max:20480', // 20 MB
|
||||
]);
|
||||
|
||||
$ticket = SupportTicket::findOrFail($ticketId);
|
||||
$admin = auth('admin')->user();
|
||||
|
||||
$data = [
|
||||
'ticket_id' => $ticketId,
|
||||
'sender_id' => $admin->id,
|
||||
'sender_type' => \App\Models\Admin::class,
|
||||
'message' => $request->message,
|
||||
|
||||
'read_by_admin' => true,
|
||||
'read_by_user' => false,
|
||||
];
|
||||
|
||||
// File Upload
|
||||
if ($request->hasFile('file')) {
|
||||
$path = $request->file('file')->store('chat', 'public');
|
||||
$data['file_path'] = $path;
|
||||
$data['file_type'] = $request->file('file')->getMimeType();
|
||||
}
|
||||
|
||||
// Save message
|
||||
$message = ChatMessage::create($data);
|
||||
$message->load('sender');
|
||||
|
||||
\Log::info("DEBUG: ChatController sendMessage called", [
|
||||
'ticket_id' => $ticketId,
|
||||
'payload' => $request->all()
|
||||
]);
|
||||
|
||||
// Broadcast real-time
|
||||
broadcast(new NewChatMessage($message));
|
||||
|
||||
\Log::info("DEBUG: ChatController sendMessage called 79", [
|
||||
'ticket_id' => $ticketId,
|
||||
'payload' => $request->all()
|
||||
]);
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,12 @@ 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%")
|
||||
@@ -31,20 +34,22 @@ class AdminCustomerController extends Controller
|
||||
});
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Orders
|
||||
$totalOrders = $customer->orders->count();
|
||||
$totalAmount = $customer->orders->sum('ttl_amount');
|
||||
$recentOrders = $customer->orders()->latest()->take(5)->get();
|
||||
$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',
|
||||
'totalAmount',
|
||||
'recentOrders'
|
||||
'totalOrderAmount',
|
||||
'totalPayable',
|
||||
'totalPaid',
|
||||
'totalRemaining'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// TOGGLE STATUS ACTIVE / INACTIVE
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Models\InvoiceItem;
|
||||
use Mpdf\Mpdf;
|
||||
use App\Models\InvoiceInstallment;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
|
||||
class AdminInvoiceController extends Controller
|
||||
{
|
||||
@@ -26,13 +27,11 @@ class AdminInvoiceController extends Controller
|
||||
// -------------------------------------------------------------
|
||||
public function popup($id)
|
||||
{
|
||||
$invoice = Invoice::with(['items', 'order'])->findOrFail($id);
|
||||
$invoice = Invoice::with(['items', 'order', 'installments'])->findOrFail($id);
|
||||
|
||||
// Find actual Shipment record
|
||||
$shipment = \App\Models\Shipment::whereHas('items', function ($q) use ($invoice) {
|
||||
$q->where('order_id', $invoice->order_id);
|
||||
})
|
||||
->first();
|
||||
})->first();
|
||||
|
||||
return view('admin.popup_invoice', compact('invoice', 'shipment'));
|
||||
}
|
||||
@@ -45,7 +44,14 @@ class AdminInvoiceController extends Controller
|
||||
$invoice = Invoice::with(['order.shipments'])->findOrFail($id);
|
||||
$shipment = $invoice->order?->shipments?->first();
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
@@ -145,6 +151,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/DELETE)
|
||||
// -------------------------------------------------------------
|
||||
@@ -183,6 +200,8 @@ class AdminInvoiceController extends Controller
|
||||
// Mark as 'paid' if GST-inclusive total is cleared
|
||||
if ($newPaid >= $invoice->final_amount_with_gst) {
|
||||
$invoice->update(['status' => 'paid']);
|
||||
|
||||
$this->generateInvoicePDF($invoice);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
@@ -210,6 +229,8 @@ class AdminInvoiceController extends Controller
|
||||
// Update status if not fully paid anymore
|
||||
if ($remaining > 0 && $invoice->status === "paid") {
|
||||
$invoice->update(['status' => 'pending']);
|
||||
|
||||
$this->generateInvoicePDF($invoice);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
|
||||
@@ -10,97 +10,40 @@ use App\Models\MarkList;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use App\Models\User;
|
||||
use PDF; // barryvdh/laravel-dompdf facade
|
||||
use PDF;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use App\Exports\OrdersExport;
|
||||
use App\Imports\OrderItemsPreviewImport;
|
||||
|
||||
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
|
||||
class AdminOrderController extends Controller
|
||||
{
|
||||
// ---------------------------
|
||||
// LIST / DASHBOARD
|
||||
// ---------------------------
|
||||
/* ---------------------------
|
||||
* LIST / DASHBOARD
|
||||
* ---------------------------*/
|
||||
public function index()
|
||||
{
|
||||
// raw list for admin dashboard (simple)
|
||||
$orders = Order::latest()->get();
|
||||
$markList = MarkList::where('status', 'active')->get();
|
||||
|
||||
return view('admin.dashboard', compact('orders', 'markList'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Orders list (detailed)
|
||||
*/
|
||||
// public function orderShow()
|
||||
// {
|
||||
// $orders = Order::with(['markList', 'shipments', 'invoice'])
|
||||
// ->latest('id')
|
||||
// ->get();
|
||||
|
||||
// return view('admin.orders', compact('orders'));
|
||||
// }
|
||||
|
||||
// ---------------------------
|
||||
// CREATE NEW ORDER (simple DB flow)
|
||||
// ---------------------------
|
||||
/**
|
||||
* Show create form (you can place create UI on separate view or dashboard)
|
||||
*/
|
||||
/* ---------------------------
|
||||
* CREATE NEW ORDER (simple page)
|
||||
* ---------------------------*/
|
||||
public function create()
|
||||
{
|
||||
// return a dedicated create view - create it at resources/views/admin/orders_create.blade.php
|
||||
// If you prefer create UI on dashboard, change this to redirect route('admin.orders.index') etc.
|
||||
$markList = MarkList::where('status', 'active')->get();
|
||||
return view('admin.orders_create', compact('markList'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new order and optionally create initial invoice
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'mark_no' => 'required|string',
|
||||
'origin' => 'nullable|string',
|
||||
'destination' => 'nullable|string',
|
||||
// totals optional when creating without items
|
||||
'ctn' => 'nullable|numeric',
|
||||
'qty' => 'nullable|numeric',
|
||||
'ttl_qty' => 'nullable|numeric',
|
||||
'ttl_amount' => 'nullable|numeric',
|
||||
'cbm' => 'nullable|numeric',
|
||||
'ttl_cbm' => 'nullable|numeric',
|
||||
'kg' => 'nullable|numeric',
|
||||
'ttl_kg' => 'nullable|numeric',
|
||||
]);
|
||||
|
||||
$order = Order::create([
|
||||
'order_id' => $this->generateOrderId(),
|
||||
'mark_no' => $data['mark_no'],
|
||||
'origin' => $data['origin'] ?? null,
|
||||
'destination' => $data['destination'] ?? null,
|
||||
'ctn' => $data['ctn'] ?? 0,
|
||||
'qty' => $data['qty'] ?? 0,
|
||||
'ttl_qty' => $data['ttl_qty'] ?? 0,
|
||||
'ttl_amount' => $data['ttl_amount'] ?? 0,
|
||||
'cbm' => $data['cbm'] ?? 0,
|
||||
'ttl_cbm' => $data['ttl_cbm'] ?? 0,
|
||||
'kg' => $data['kg'] ?? 0,
|
||||
'ttl_kg' => $data['ttl_kg'] ?? 0,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
//If you want to auto-create an invoice at order creation, uncomment:
|
||||
$this->createInvoice($order);
|
||||
|
||||
return redirect()->route('admin.orders.show', $order->id)
|
||||
->with('success', 'Order created successfully.');
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// SHOW / POPUP
|
||||
// ---------------------------
|
||||
/* ---------------------------
|
||||
* SHOW / POPUP
|
||||
* ---------------------------*/
|
||||
public function show($id)
|
||||
{
|
||||
$order = Order::with('items', 'markList')->findOrFail($id);
|
||||
@@ -109,71 +52,72 @@ class AdminOrderController extends Controller
|
||||
return view('admin.orders_show', compact('order', 'user'));
|
||||
}
|
||||
|
||||
// public function popup($id)
|
||||
// {
|
||||
// $order = Order::with(['items', 'markList'])->findOrFail($id);
|
||||
// $user = $this->getCustomerFromOrder($order);
|
||||
|
||||
// return view('admin.popup', compact('order', 'user'));
|
||||
// }
|
||||
|
||||
// ---------------------------
|
||||
// ORDER ITEM MANAGEMENT (DB)
|
||||
// ---------------------------
|
||||
/**
|
||||
* Add an item to an existing order
|
||||
*/
|
||||
public function addItem(Request $request, $orderId)
|
||||
public function popup($id)
|
||||
{
|
||||
$order = Order::with(['items', 'markList'])->findOrFail($id);
|
||||
|
||||
$user = null;
|
||||
if ($order->markList && $order->markList->customer_id) {
|
||||
$user = User::where('customer_id', $order->markList->customer_id)->first();
|
||||
}
|
||||
|
||||
return view('admin.popup', compact('order', 'user'));
|
||||
}
|
||||
|
||||
/* ---------------------------
|
||||
* ORDER ITEM MANAGEMENT (existing orders)
|
||||
* ---------------------------*/
|
||||
public function addItem(Request $request, $orderId)
|
||||
{
|
||||
$order = Order::findOrFail($orderId);
|
||||
|
||||
$data = $request->validate([
|
||||
'description' => 'required|string',
|
||||
'ctn' => 'nullable|numeric',
|
||||
'qty' => 'nullable|numeric',
|
||||
'ttl_qty' => 'nullable|numeric',
|
||||
'unit' => 'nullable|string',
|
||||
'price' => 'nullable|numeric',
|
||||
'ttl_amount' => 'nullable|numeric',
|
||||
'cbm' => 'nullable|numeric',
|
||||
'ttl_cbm' => 'nullable|numeric',
|
||||
'kg' => 'nullable|numeric',
|
||||
'ttl_kg' => 'nullable|numeric',
|
||||
'shop_no' => 'nullable|string',
|
||||
]);
|
||||
|
||||
// ✅ BACKEND CALCULATION
|
||||
$ctn = (float) ($data['ctn'] ?? 0);
|
||||
$qty = (float) ($data['qty'] ?? 0);
|
||||
$price = (float) ($data['price'] ?? 0);
|
||||
$cbm = (float) ($data['cbm'] ?? 0);
|
||||
$kg = (float) ($data['kg'] ?? 0);
|
||||
|
||||
$data['ttl_qty'] = $ctn * $qty;
|
||||
$data['ttl_amount'] = $data['ttl_qty'] * $price;
|
||||
$data['ttl_cbm'] = $cbm * $ctn;
|
||||
$data['ttl_kg'] = $ctn * $kg;
|
||||
|
||||
$data['order_id'] = $order->id;
|
||||
|
||||
OrderItem::create($data);
|
||||
|
||||
// recalc totals and save to order
|
||||
$this->recalcTotals($order);
|
||||
$this->updateInvoiceFromOrder($order); // <-- NEW
|
||||
$this->updateInvoiceFromOrder($order);
|
||||
|
||||
return redirect()->back()->with('success', 'Item added and totals updated.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Soft-delete an order item and recalc totals
|
||||
*/
|
||||
public function deleteItem($id)
|
||||
{
|
||||
$item = OrderItem::findOrFail($id);
|
||||
$order = $item->order;
|
||||
|
||||
$item->delete(); // soft delete
|
||||
$item->delete();
|
||||
|
||||
// recalc totals
|
||||
$this->recalcTotals($order);
|
||||
$this->updateInvoiceFromOrder($order);
|
||||
|
||||
|
||||
return redirect()->back()->with('success', 'Item deleted and totals updated.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore soft-deleted item and recalc totals
|
||||
*/
|
||||
public function restoreItem($id)
|
||||
{
|
||||
$item = OrderItem::withTrashed()->findOrFail($id);
|
||||
@@ -181,17 +125,15 @@ class AdminOrderController extends Controller
|
||||
|
||||
$item->restore();
|
||||
|
||||
// recalc totals
|
||||
$this->recalcTotals($order);
|
||||
$this->updateInvoiceFromOrder($order);
|
||||
|
||||
|
||||
return redirect()->back()->with('success', 'Item restored and totals updated.');
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// ORDER CRUD: update / destroy
|
||||
// ---------------------------
|
||||
/* ---------------------------
|
||||
* ORDER CRUD: update / destroy
|
||||
* ---------------------------*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$order = Order::findOrFail($id);
|
||||
@@ -208,46 +150,35 @@ class AdminOrderController extends Controller
|
||||
'destination' => $data['destination'] ?? null,
|
||||
]);
|
||||
|
||||
// optionally recalc totals (not necessary unless you change item-level fields here)
|
||||
$this->recalcTotals($order);
|
||||
|
||||
return redirect()->route('admin.orders.show', $order->id)
|
||||
->with('success', 'Order updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft-delete whole order and its items (soft-delete items first then order)
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
$order = Order::findOrFail($id);
|
||||
|
||||
// soft-delete items first (so they show up in onlyTrashed for restore)
|
||||
OrderItem::where('order_id', $order->id)->delete();
|
||||
|
||||
// then soft-delete order
|
||||
$order->delete();
|
||||
|
||||
return redirect()->route('admin.orders.index')
|
||||
->with('success', 'Order deleted successfully.');
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// HELPERS
|
||||
// ---------------------------
|
||||
/**
|
||||
* Recalculate totals for the order from current (non-deleted) items
|
||||
*/
|
||||
/* ---------------------------
|
||||
* HELPERS
|
||||
* ---------------------------*/
|
||||
private function recalcTotals(Order $order)
|
||||
{
|
||||
// make sure we re-query live items (non-deleted)
|
||||
$items = $order->items()->get();
|
||||
|
||||
$order->update([
|
||||
'ctn' => (int) $items->sum(fn($i) => (int) ($i->ctn ?? 0)),
|
||||
'qty' => (int) $items->sum(fn($i) => (int) ($i->qty ?? 0)),
|
||||
'ttl_qty' => (int) $items->sum(fn($i) => (int) ($i->ttl_qty ?? 0)),
|
||||
'ttl_amount'=> (float) $items->sum(fn($i) => (float) ($i->ttl_amount ?? 0)),
|
||||
'ttl_amount' => (float) $items->sum(fn($i) => (float) ($i->ttl_amount ?? 0)),
|
||||
'cbm' => (float) $items->sum(fn($i) => (float) ($i->cbm ?? 0)),
|
||||
'ttl_cbm' => (float) $items->sum(fn($i) => (float) ($i->ttl_cbm ?? 0)),
|
||||
'kg' => (float) $items->sum(fn($i) => (float) ($i->kg ?? 0)),
|
||||
@@ -255,9 +186,6 @@ class AdminOrderController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate order id (keeps old format)
|
||||
*/
|
||||
private function generateOrderId()
|
||||
{
|
||||
$year = date('y');
|
||||
@@ -269,9 +197,9 @@ class AdminOrderController extends Controller
|
||||
return $prefix . str_pad($nextNumber, 8, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// INVOICE CREATION (optional helper used by store/finish)
|
||||
// ---------------------------
|
||||
/* ---------------------------
|
||||
* INVOICE CREATION HELPERS
|
||||
* ---------------------------*/
|
||||
private function createInvoice(Order $order)
|
||||
{
|
||||
$invoiceNumber = $this->generateInvoiceNumber();
|
||||
@@ -302,7 +230,6 @@ class AdminOrderController extends Controller
|
||||
'pdf_path' => null,
|
||||
]);
|
||||
|
||||
// clone order items into invoice items
|
||||
foreach ($order->items as $item) {
|
||||
InvoiceItem::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
@@ -350,198 +277,277 @@ class AdminOrderController extends Controller
|
||||
return null;
|
||||
}
|
||||
|
||||
public function popup($id)
|
||||
/* ---------------------------
|
||||
* SEE (detailed)
|
||||
* ---------------------------*/
|
||||
public function see($id)
|
||||
{
|
||||
// Load order with items + markList
|
||||
$order = Order::with(['items', 'markList'])->findOrFail($id);
|
||||
$order = Order::with([
|
||||
'markList',
|
||||
'items',
|
||||
'invoice.items',
|
||||
'shipments' => function ($q) use ($id) {
|
||||
$q->whereHas('orders', function ($oq) use ($id) {
|
||||
$oq->where('orders.id', $id)
|
||||
->whereNull('orders.deleted_at');
|
||||
})->with([
|
||||
'orders' => function ($oq) use ($id) {
|
||||
$oq->where('orders.id', $id)
|
||||
->whereNull('orders.deleted_at')
|
||||
->with('items');
|
||||
}
|
||||
]);
|
||||
}
|
||||
])->findOrFail($id);
|
||||
|
||||
// Fetch user based on markList customer_id (same as show method)
|
||||
$user = null;
|
||||
if ($order->markList && $order->markList->customer_id) {
|
||||
$user = \App\Models\User::where('customer_id', $order->markList->customer_id)->first();
|
||||
$orderData = [
|
||||
'order_id' => $order->order_id,
|
||||
'status' => $order->status,
|
||||
'totals' => [
|
||||
'ctn' => $order->ctn,
|
||||
'qty' => $order->qty,
|
||||
'ttl_qty' => $order->ttl_qty,
|
||||
'cbm' => $order->cbm,
|
||||
'ttl_cbm' => $order->ttl_cbm,
|
||||
'kg' => $order->kg,
|
||||
'ttl_kg' => $order->ttl_kg,
|
||||
'amount' => $order->ttl_amount,
|
||||
],
|
||||
'items' => $order->items,
|
||||
];
|
||||
|
||||
$shipmentsData = [];
|
||||
foreach ($order->shipments as $shipment) {
|
||||
$shipmentOrders = [];
|
||||
$totals = [
|
||||
'ctn' => 0, 'qty' => 0, 'ttl_qty' => 0,
|
||||
'cbm' => 0, 'ttl_cbm' => 0,
|
||||
'kg' => 0, 'ttl_kg' => 0,
|
||||
'amount' => 0,
|
||||
];
|
||||
|
||||
foreach ($shipment->orders as $shipOrder) {
|
||||
foreach ($shipOrder->items as $item) {
|
||||
$shipmentOrders[] = [
|
||||
'order_id' => $shipOrder->order_id,
|
||||
'origin' => $shipOrder->origin,
|
||||
'destination' => $shipOrder->destination,
|
||||
'description' => $item->description,
|
||||
'ctn' => $item->ctn,
|
||||
'qty' => $item->qty,
|
||||
'ttl_qty' => $item->ttl_qty,
|
||||
'amount' => $item->ttl_amount,
|
||||
];
|
||||
|
||||
$totals['ctn'] += $item->ctn;
|
||||
$totals['qty'] += $item->qty;
|
||||
$totals['ttl_qty'] += $item->ttl_qty;
|
||||
$totals['cbm'] += $item->cbm;
|
||||
$totals['ttl_cbm'] += $item->ttl_cbm;
|
||||
$totals['kg'] += $item->kg;
|
||||
$totals['ttl_kg'] += $item->ttl_kg;
|
||||
$totals['amount'] += $item->ttl_amount;
|
||||
}
|
||||
}
|
||||
|
||||
return view('admin.popup', compact('order', 'user'));
|
||||
if (empty($shipmentOrders)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public function resetTemp()
|
||||
{
|
||||
session()->forget(['temp_order_items', 'mark_no', 'origin', 'destination']);
|
||||
|
||||
return redirect()->to(route('admin.orders.index') . '#createOrderForm')
|
||||
->with('success', 'Order reset successfully.');
|
||||
$shipmentsData[] = [
|
||||
'shipment_id' => $shipment->shipment_id,
|
||||
'status' => $shipment->status,
|
||||
'date' => $shipment->shipment_date,
|
||||
'total_orders' => 1,
|
||||
'orders' => $shipmentOrders,
|
||||
'totals' => $totals,
|
||||
];
|
||||
}
|
||||
|
||||
$invoiceData = null;
|
||||
if ($order->invoice) {
|
||||
$invoice = $order->invoice;
|
||||
$invoiceData = [
|
||||
'invoice_no' => $invoice->invoice_number,
|
||||
'status' => $invoice->status,
|
||||
'invoice_date' => $invoice->invoice_date,
|
||||
'due_date' => $invoice->due_date,
|
||||
'customer' => [
|
||||
'name' => $invoice->customer_name,
|
||||
'mobile' => $invoice->customer_mobile,
|
||||
'email' => $invoice->customer_email,
|
||||
'address' => $invoice->customer_address,
|
||||
'pincode' => $invoice->pincode,
|
||||
],
|
||||
'items' => $invoice->items,
|
||||
'summary' => [
|
||||
'amount' => $invoice->final_amount,
|
||||
'cgst' => 0,
|
||||
'sgst' => 0,
|
||||
'total' => $invoice->final_amount_with_gst,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return view('admin.see_order', compact(
|
||||
'order',
|
||||
'orderData',
|
||||
'shipmentsData',
|
||||
'invoiceData'
|
||||
));
|
||||
}
|
||||
|
||||
/* ---------------------------
|
||||
* FILTERED LIST + EXPORTS
|
||||
* ---------------------------*/
|
||||
public function orderShow()
|
||||
{
|
||||
$orders = Order::with([
|
||||
'markList', // company, customer, origin, destination, date
|
||||
'shipments', // shipment_id, shipment_date, status
|
||||
'invoice' // invoice number, dates, amounts, status
|
||||
])
|
||||
->latest('id') // show latest orders first
|
||||
->get();
|
||||
'markList',
|
||||
'shipments',
|
||||
'invoice'
|
||||
])->latest('id')->get();
|
||||
|
||||
return view('admin.orders', compact('orders'));
|
||||
}
|
||||
|
||||
// inside AdminOrderController
|
||||
|
||||
private function buildOrdersQueryFromRequest(Request $request)
|
||||
{
|
||||
$query = Order::with(['markList', 'invoice', 'shipments']);
|
||||
$query = Order::query()
|
||||
->with(['markList', 'invoice', 'shipments']);
|
||||
|
||||
// Search across order_id, markList.company_name, markList.customer_id, invoice.invoice_number
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function($q) use ($search) {
|
||||
$q->where('order_id', 'like', "%{$search}%")
|
||||
->orWhereHas('markList', function($q2) use ($search) {
|
||||
$search = trim($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) {
|
||||
$q4->where('shipments.shipment_id', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Invoice status filter
|
||||
if ($request->filled('status')) {
|
||||
$query->whereHas('invoice', function($q) use ($request) {
|
||||
$q->where('status', $request->status);
|
||||
$query->where(function ($q) use ($request) {
|
||||
$q->whereHas('invoice', function ($q2) use ($request) {
|
||||
$q2->where('status', $request->status);
|
||||
})->orWhereDoesntHave('invoice');
|
||||
});
|
||||
}
|
||||
|
||||
// Shipment status filter
|
||||
if ($request->filled('shipment')) {
|
||||
$query->whereHas('shipments', function($q) use ($request) {
|
||||
$q->where('status', $request->shipment);
|
||||
$query->where(function ($q) use ($request) {
|
||||
$q->whereHas('shipments', function ($q2) use ($request) {
|
||||
$q2->where('status', $request->shipment);
|
||||
})->orWhereDoesntHave('shipments');
|
||||
});
|
||||
}
|
||||
|
||||
// optional ordering
|
||||
$query->latest('id');
|
||||
if ($request->filled('from_date')) {
|
||||
$query->whereDate('orders.created_at', '>=', $request->from_date);
|
||||
}
|
||||
|
||||
return $query;
|
||||
if ($request->filled('to_date')) {
|
||||
$query->whereDate('orders.created_at', '<=', $request->to_date);
|
||||
}
|
||||
|
||||
return $query->latest('orders.id');
|
||||
}
|
||||
|
||||
public function downloadPdf(Request $request)
|
||||
{
|
||||
// Build same filtered query used for table
|
||||
$query = $this->buildOrdersQueryFromRequest($request);
|
||||
|
||||
$orders = $query->get();
|
||||
|
||||
// optional: pass filters to view for header
|
||||
$orders = $this->buildOrdersQueryFromRequest($request)->get();
|
||||
$filters = [
|
||||
'search' => $request->search ?? null,
|
||||
'status' => $request->status ?? null,
|
||||
'shipment' => $request->shipment ?? null,
|
||||
'search' => $request->search,
|
||||
'status' => $request->status,
|
||||
'shipment' => $request->shipment,
|
||||
'from' => $request->from_date,
|
||||
'to' => $request->to_date,
|
||||
];
|
||||
|
||||
$pdf = PDF::loadView('admin.orders.pdf', compact('orders', 'filters'))
|
||||
->setPaper('a4', 'landscape'); // adjust if needed
|
||||
->setPaper('a4', 'landscape');
|
||||
|
||||
$fileName = 'orders-report'
|
||||
. ($filters['status'] ? "-{$filters['status']}" : '')
|
||||
. '-' . date('Y-m-d') . '.pdf';
|
||||
|
||||
return $pdf->download($fileName);
|
||||
return $pdf->download(
|
||||
'orders-report-' . now()->format('Y-m-d') . '.pdf'
|
||||
);
|
||||
}
|
||||
|
||||
public function downloadExcel(Request $request)
|
||||
{
|
||||
// pass request to OrdersExport which will build Filtered query internally
|
||||
return Excel::download(new OrdersExport($request), 'orders-report-' . date('Y-m-d') . '.xlsx');
|
||||
return Excel::download(
|
||||
new OrdersExport($request),
|
||||
'orders-report-' . now()->format('Y-m-d') . '.xlsx'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/* --------------------------------------------------
|
||||
* NEW: Create Order + Invoice directly from popup
|
||||
* route: admin.orders.temp.add (Create New Order form)
|
||||
* --------------------------------------------------*/
|
||||
public function addTempItem(Request $request)
|
||||
{
|
||||
// Validate item fields
|
||||
$item = $request->validate([
|
||||
'mark_no' => 'required',
|
||||
'origin' => 'required',
|
||||
'destination' => 'required',
|
||||
'description' => 'required|string',
|
||||
'ctn' => 'nullable|numeric',
|
||||
'qty' => 'nullable|numeric',
|
||||
'ttl_qty' => 'nullable|numeric',
|
||||
'unit' => 'nullable|string',
|
||||
'price' => 'nullable|numeric',
|
||||
'ttl_amount' => 'nullable|numeric',
|
||||
'cbm' => 'nullable|numeric',
|
||||
'ttl_cbm' => 'nullable|numeric',
|
||||
'kg' => 'nullable|numeric',
|
||||
'ttl_kg' => 'nullable|numeric',
|
||||
'shop_no' => 'nullable|string',
|
||||
]);
|
||||
|
||||
// ❌ Prevent changing mark_no once first item added
|
||||
if (session()->has('mark_no') && session('mark_no') != $request->mark_no) {
|
||||
return redirect()->to(route('admin.orders.index') . '#createOrderForm')
|
||||
->with('error', 'You must finish or clear the current order before changing Mark No.');
|
||||
}
|
||||
|
||||
// Save mark, origin, destination ONLY ONCE
|
||||
if (!session()->has('mark_no')) {
|
||||
session([
|
||||
'mark_no' => $request->mark_no,
|
||||
'origin' => $request->origin,
|
||||
'destination' => $request->destination
|
||||
]);
|
||||
}
|
||||
|
||||
// ❌ DO NOT overwrite these values again
|
||||
// session(['mark_no' => $request->mark_no]);
|
||||
// session(['origin' => $request->origin]);
|
||||
// session(['destination' => $request->destination]);
|
||||
|
||||
// Add new sub-item into session
|
||||
session()->push('temp_order_items', $item);
|
||||
|
||||
return redirect()->to(route('admin.orders.index') . '#createOrderForm')
|
||||
|
||||
->with('success', 'Item added.');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// STEP 3 : FINISH ORDER
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function finishOrder(Request $request)
|
||||
{
|
||||
// 1) order-level fields
|
||||
$request->validate([
|
||||
'mark_no' => 'required',
|
||||
'origin' => 'required',
|
||||
'destination' => 'required',
|
||||
]);
|
||||
|
||||
$items = session('temp_order_items', []);
|
||||
// 2) multi-row items
|
||||
$items = $request->validate([
|
||||
'items' => 'required|array',
|
||||
'items.*.description' => 'required|string',
|
||||
'items.*.ctn' => 'nullable|numeric',
|
||||
'items.*.qty' => 'nullable|numeric',
|
||||
|
||||
'items.*.unit' => 'nullable|string',
|
||||
'items.*.price' => 'nullable|numeric',
|
||||
|
||||
'items.*.cbm' => 'nullable|numeric',
|
||||
|
||||
'items.*.kg' => 'nullable|numeric',
|
||||
|
||||
'items.*.shop_no' => 'nullable|string',
|
||||
])['items'];
|
||||
|
||||
// रिकामे rows काढा
|
||||
$items = array_filter($items, function ($row) {
|
||||
return trim($row['description'] ?? '') !== '';
|
||||
});
|
||||
|
||||
if (empty($items)) {
|
||||
return redirect()->to(route('admin.orders.index') . '#createOrderForm')
|
||||
->with('error', 'Add at least one item before finishing.');
|
||||
return back()->with('error', 'Add at least one item.');
|
||||
}
|
||||
|
||||
// =======================
|
||||
// GENERATE ORDER ID
|
||||
// =======================
|
||||
$year = date('y');
|
||||
$prefix = "KNT-$year-";
|
||||
// ✅ BACKEND CALCULATION (DO NOT TRUST FRONTEND)
|
||||
foreach ($items as &$item) {
|
||||
|
||||
$lastOrder = Order::latest('id')->first();
|
||||
$nextNumber = $lastOrder ? intval(substr($lastOrder->order_id, -8)) + 1 : 1;
|
||||
$ctn = (float) ($item['ctn'] ?? 0);
|
||||
$qty = (float) ($item['qty'] ?? 0);
|
||||
$price = (float) ($item['price'] ?? 0);
|
||||
$cbm = (float) ($item['cbm'] ?? 0);
|
||||
$kg = (float) ($item['kg'] ?? 0);
|
||||
|
||||
$orderId = $prefix . str_pad($nextNumber, 8, '0', STR_PAD_LEFT);
|
||||
// Calculated fields
|
||||
$item['ttl_qty'] = $ctn * $qty;
|
||||
$item['ttl_amount'] = $item['ttl_qty'] * $price;
|
||||
$item['ttl_cbm'] = $cbm * $ctn;
|
||||
$item['ttl_kg'] = $ctn * $kg;
|
||||
}
|
||||
unset($item); // VERY IMPORTANT
|
||||
|
||||
// =======================
|
||||
// TOTAL SUMS
|
||||
// =======================
|
||||
|
||||
// 3) totals
|
||||
$total_ctn = array_sum(array_column($items, 'ctn'));
|
||||
$total_qty = array_sum(array_column($items, 'qty'));
|
||||
$total_ttl_qty = array_sum(array_column($items, 'ttl_qty'));
|
||||
@@ -551,14 +557,15 @@ class AdminOrderController extends Controller
|
||||
$total_kg = array_sum(array_column($items, 'kg'));
|
||||
$total_ttl_kg = array_sum(array_column($items, 'ttl_kg'));
|
||||
|
||||
// =======================
|
||||
// CREATE ORDER
|
||||
// =======================
|
||||
// 4) order id generate
|
||||
$orderId = $this->generateOrderId();
|
||||
|
||||
// 5) order create
|
||||
$order = Order::create([
|
||||
'order_id' => $orderId,
|
||||
'mark_no' => $request->mark_no,
|
||||
'origin' => $request->origin,
|
||||
'destination' => $request->destination,
|
||||
'destination'=> $request->destination,
|
||||
'ctn' => $total_ctn,
|
||||
'qty' => $total_qty,
|
||||
'ttl_qty' => $total_ttl_qty,
|
||||
@@ -567,14 +574,14 @@ class AdminOrderController extends Controller
|
||||
'ttl_cbm' => $total_ttl_cbm,
|
||||
'kg' => $total_kg,
|
||||
'ttl_kg' => $total_ttl_kg,
|
||||
'status' => 'pending',
|
||||
'status' => 'order_placed',
|
||||
]);
|
||||
|
||||
// SAVE ORDER ITEMS
|
||||
// 6) order items
|
||||
foreach ($items as $item) {
|
||||
OrderItem::create([
|
||||
'order_id' => $order->id,
|
||||
'description' => $item['description'],
|
||||
'description'=> $item['description'],
|
||||
'ctn' => $item['ctn'],
|
||||
'qty' => $item['qty'],
|
||||
'ttl_qty' => $item['ttl_qty'],
|
||||
@@ -589,56 +596,44 @@ class AdminOrderController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
// =======================
|
||||
// INVOICE CREATION START
|
||||
// =======================
|
||||
// 7) invoice number
|
||||
$invoiceNumber = $this->generateInvoiceNumber();
|
||||
|
||||
// 1. Auto-generate invoice number
|
||||
$lastInvoice = \App\Models\Invoice::latest()->first();
|
||||
$nextInvoice = $lastInvoice ? $lastInvoice->id + 1 : 1;
|
||||
$invoiceNumber = 'INV-' . date('Y') . '-' . str_pad($nextInvoice, 6, '0', STR_PAD_LEFT);
|
||||
|
||||
// 2. Fetch customer (using mark list → customer_id)
|
||||
// 8) customer fetch
|
||||
$markList = MarkList::where('mark_no', $order->mark_no)->first();
|
||||
$customer = null;
|
||||
|
||||
if ($markList && $markList->customer_id) {
|
||||
$customer = \App\Models\User::where('customer_id', $markList->customer_id)->first();
|
||||
$customer = User::where('customer_id', $markList->customer_id)->first();
|
||||
}
|
||||
|
||||
// 3. Create Invoice Record
|
||||
$invoice = \App\Models\Invoice::create([
|
||||
// 9) invoice create
|
||||
$invoice = Invoice::create([
|
||||
'order_id' => $order->id,
|
||||
'customer_id' => $customer->id ?? null,
|
||||
'mark_no' => $order->mark_no,
|
||||
|
||||
'invoice_number' => $invoiceNumber,
|
||||
'invoice_date' => now(),
|
||||
'due_date' => now()->addDays(10),
|
||||
|
||||
'payment_method' => null,
|
||||
'reference_no' => null,
|
||||
'status' => 'pending',
|
||||
|
||||
'final_amount' => $total_amount,
|
||||
'gst_percent' => 0,
|
||||
'gst_amount' => 0,
|
||||
'final_amount_with_gst' => $total_amount,
|
||||
|
||||
// snapshot customer fields
|
||||
'customer_name' => $customer->customer_name ?? null,
|
||||
'company_name' => $customer->company_name ?? null,
|
||||
'customer_email' => $customer->email ?? null,
|
||||
'customer_mobile' => $customer->mobile_no ?? null,
|
||||
'customer_address' => $customer->address ?? null,
|
||||
'pincode' => $customer->pincode ?? null,
|
||||
|
||||
'notes' => null,
|
||||
'pdf_path' => null,
|
||||
]);
|
||||
|
||||
// 4. Clone order items into invoice_items
|
||||
// 10) invoice items
|
||||
foreach ($order->items as $item) {
|
||||
\App\Models\InvoiceItem::create([
|
||||
InvoiceItem::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
'description' => $item->description,
|
||||
'ctn' => $item->ctn,
|
||||
@@ -655,49 +650,56 @@ class AdminOrderController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
// 5. TODO: PDF generation (I will add this later)
|
||||
$invoice->pdf_path = null; // placeholder for now
|
||||
$invoice->save();
|
||||
|
||||
// =======================
|
||||
// END INVOICE CREATION
|
||||
// =======================
|
||||
|
||||
// CLEAR TEMP DATA
|
||||
session()->forget(['temp_order_items', 'mark_no', 'origin', 'destination']);
|
||||
|
||||
return redirect()->route('admin.orders.index')
|
||||
->with('success', 'Order + Invoice created successfully.');
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// ORDER CRUD: update / destroy
|
||||
// ---------------------------
|
||||
/* ---------------------------
|
||||
* UPDATE ORDER ITEM (existing orders)
|
||||
* ---------------------------*/
|
||||
public function updateItem(Request $request, $id)
|
||||
{
|
||||
{
|
||||
$item = OrderItem::findOrFail($id);
|
||||
$order = $item->order;
|
||||
|
||||
$request->validate([
|
||||
'description' => 'required|string',
|
||||
'ctn' => 'nullable|numeric',
|
||||
'qty' => 'nullable|numeric',
|
||||
'unit' => 'nullable|string',
|
||||
'price' => 'nullable|numeric',
|
||||
'cbm' => 'nullable|numeric',
|
||||
'kg' => 'nullable|numeric',
|
||||
'shop_no' => 'nullable|string',
|
||||
]);
|
||||
|
||||
// ✅ BACKEND CALCULATION
|
||||
$ctn = (float) ($request->ctn ?? 0);
|
||||
$qty = (float) ($request->qty ?? 0);
|
||||
$price = (float) ($request->price ?? 0);
|
||||
$cbm = (float) ($request->cbm ?? 0);
|
||||
$kg = (float) ($request->kg ?? 0);
|
||||
|
||||
$item->update([
|
||||
'description' => $request->description,
|
||||
'ctn' => $request->ctn,
|
||||
'qty' => $request->qty,
|
||||
'ttl_qty' => $request->ttl_qty,
|
||||
'ctn' => $ctn,
|
||||
'qty' => $qty,
|
||||
'ttl_qty' => $ctn * $qty,
|
||||
'unit' => $request->unit,
|
||||
'price' => $request->price,
|
||||
'ttl_amount' => $request->ttl_amount,
|
||||
'cbm' => $request->cbm,
|
||||
'ttl_cbm' => $request->ttl_cbm,
|
||||
'kg' => $request->kg,
|
||||
'ttl_kg' => $request->ttl_kg,
|
||||
'price' => $price,
|
||||
'ttl_amount' => ($ctn * $qty) * $price,
|
||||
'cbm' => $cbm,
|
||||
'ttl_cbm' => $cbm * $ctn,
|
||||
'kg' => $kg,
|
||||
'ttl_kg' => $ctn * $kg,
|
||||
'shop_no' => $request->shop_no,
|
||||
]);
|
||||
|
||||
$this->recalcTotals($order);
|
||||
$this->updateInvoiceFromOrder($order); // <-- NEW
|
||||
$this->updateInvoiceFromOrder($order);
|
||||
|
||||
return back()->with('success', 'Item updated successfully');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function updateInvoiceFromOrder(Order $order)
|
||||
@@ -705,20 +707,17 @@ class AdminOrderController extends Controller
|
||||
$invoice = Invoice::where('order_id', $order->id)->first();
|
||||
|
||||
if (!$invoice) {
|
||||
return; // No invoice exists (should not happen normally)
|
||||
return;
|
||||
}
|
||||
|
||||
// Update invoice totals
|
||||
$invoice->final_amount = $order->ttl_amount;
|
||||
$invoice->gst_percent = 0;
|
||||
$invoice->gst_amount = 0;
|
||||
$invoice->final_amount_with_gst = $order->ttl_amount;
|
||||
$invoice->save();
|
||||
|
||||
// Delete old invoice items
|
||||
InvoiceItem::where('invoice_id', $invoice->id)->delete();
|
||||
|
||||
// Re-create invoice items from updated order items
|
||||
foreach ($order->items as $item) {
|
||||
InvoiceItem::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
@@ -738,4 +737,35 @@ class AdminOrderController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function uploadExcelPreview(Request $request)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'excel' => 'required|file|mimes:xlsx,xls'
|
||||
]);
|
||||
|
||||
$import = new OrderItemsPreviewImport();
|
||||
Excel::import($import, $request->file('excel'));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'items' => $import->rows
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Invalid Excel file format'
|
||||
], 422);
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error($e);
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Server error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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,28 +149,34 @@ 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'
|
||||
]);
|
||||
|
||||
// 1) Update shipment status
|
||||
$shipment = Shipment::findOrFail($request->shipment_id);
|
||||
$shipment->status = $request->status;
|
||||
$shipment->save();
|
||||
|
||||
// 2) Update ALL related orders' status
|
||||
// ✅ Sync shipment status to orders ONLY after shipment exists
|
||||
foreach ($shipment->orders as $order) {
|
||||
$order->status = $shipment->status; // status is string: pending, in_transit, dispatched, delivered
|
||||
|
||||
// Prevent rollback or overwrite
|
||||
if ($order->status === 'delivered') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$order->status = $shipment->status;
|
||||
$order->save();
|
||||
}
|
||||
|
||||
return redirect()->back()->with(
|
||||
'success',
|
||||
"Shipment status updated to {$shipment->statusLabel()} and related orders updated."
|
||||
"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.');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
$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('📥 Current Token:', ['token' => (string) $currentToken]);
|
||||
|
||||
// Try refreshing token
|
||||
$newToken = JWTAuth::refresh($currentToken);
|
||||
|
||||
\Log::info('✅ Token refreshed successfully', ['new_token' => $newToken]);
|
||||
Log::info('✅ [JWT-REFRESH] Token refreshed');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'token' => $newToken,
|
||||
]);
|
||||
|
||||
} 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 (\PHPOpenSourceSaver\JWTAuth\Exceptions\TokenExpiredException $e) {
|
||||
Log::warning('⛔ [JWT-REFRESH] Refresh TTL expired');
|
||||
|
||||
} 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.',
|
||||
'message' => 'Refresh expired. Please login again.',
|
||||
], 401);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('❌ General Exception in refreshToken()', [
|
||||
'message' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
Log::error('🔥 [JWT-REFRESH] Exception', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unexpected error while refreshing token.',
|
||||
], 500);
|
||||
}
|
||||
'message' => 'Unable to refresh token.',
|
||||
], 401);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* User Login
|
||||
|
||||
96
app/Http/Controllers/user/ChatController.php
Normal file
96
app/Http/Controllers/user/ChatController.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\SupportTicket;
|
||||
use App\Models\ChatMessage;
|
||||
use App\Events\NewChatMessage;
|
||||
|
||||
class ChatController extends Controller
|
||||
{
|
||||
/**
|
||||
* Start chat or return existing ticket for this user
|
||||
*/
|
||||
public function startChat()
|
||||
{
|
||||
// One chat ticket per user
|
||||
$ticket = SupportTicket::firstOrCreate([
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'ticket' => $ticket
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all messages for this ticket
|
||||
*/
|
||||
public function getMessages($ticketId)
|
||||
{
|
||||
// Ensure this ticket belongs to the logged-in user
|
||||
$ticket = SupportTicket::where('id', $ticketId)
|
||||
->where('user_id', auth()->id())
|
||||
->firstOrFail();
|
||||
|
||||
$messages = ChatMessage::where('ticket_id', $ticketId)
|
||||
->orderBy('created_at', 'asc')
|
||||
->with('sender')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'messages' => $messages
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send text or file message from user → admin/staff
|
||||
*/
|
||||
public function sendMessage(Request $request, $ticketId)
|
||||
{
|
||||
$request->validate([
|
||||
'message' => 'nullable|string',
|
||||
'file' => 'nullable|file|max:20480', // 20MB limit
|
||||
]);
|
||||
|
||||
// Validate ticket ownership
|
||||
$ticket = SupportTicket::where('id', $ticketId)
|
||||
->where('user_id', auth()->id())
|
||||
->firstOrFail();
|
||||
|
||||
$data = [
|
||||
'ticket_id' => $ticketId,
|
||||
'sender_id' => auth()->id(),
|
||||
'sender_type' => \App\Models\User::class,
|
||||
'message' => $request->message,
|
||||
'client_id' => $request->client_id, // ✅ ADD
|
||||
'read_by_admin' => false,
|
||||
'read_by_user' => true,
|
||||
];
|
||||
|
||||
// Handle file upload
|
||||
if ($request->hasFile('file')) {
|
||||
$path = $request->file('file')->store('chat', 'public');
|
||||
$data['file_path'] = $path;
|
||||
$data['file_type'] = $request->file('file')->getMimeType();
|
||||
}
|
||||
|
||||
// Save message
|
||||
$message = ChatMessage::create($data);
|
||||
|
||||
// Load sender info for broadcast
|
||||
$message->load('sender');
|
||||
|
||||
// Fire real-time event
|
||||
broadcast(new NewChatMessage($message));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ class JwtRefreshMiddleware
|
||||
{
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
|
||||
try {
|
||||
JWTAuth::parseToken()->authenticate();
|
||||
} catch (TokenExpiredException $e) {
|
||||
|
||||
26
app/Imports/OrderItemsPreviewImport.php
Normal file
26
app/Imports/OrderItemsPreviewImport.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = [
|
||||
|
||||
40
app/Models/ChatMessage.php
Normal file
40
app/Models/ChatMessage.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class ChatMessage extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'ticket_id',
|
||||
'sender_id',
|
||||
'sender_type',
|
||||
'message',
|
||||
'file_path',
|
||||
'file_type',
|
||||
'read_by_admin',
|
||||
'read_by_user',
|
||||
'client_id',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* The ticket this message belongs to.
|
||||
*/
|
||||
public function ticket()
|
||||
{
|
||||
return $this->belongsTo(SupportTicket::class, 'ticket_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Polymorphic sender (User or Admin)
|
||||
*/
|
||||
public function sender()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
@@ -92,5 +92,20 @@ class Invoice extends Model
|
||||
return $this->hasMany(InvoiceInstallment::class);
|
||||
}
|
||||
|
||||
// App\Models\Invoice.php
|
||||
|
||||
public function totalPaid()
|
||||
{
|
||||
return $this->installments()->sum('amount');
|
||||
}
|
||||
|
||||
public function remainingAmount()
|
||||
{
|
||||
return max(
|
||||
($this->final_amount_with_gst ?? 0) - $this->totalPaid(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
32
app/Models/SupportTicket.php
Normal file
32
app/Models/SupportTicket.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class SupportTicket extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'status',
|
||||
];
|
||||
|
||||
/**
|
||||
* The user (customer) who owns this ticket.
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* All chat messages for this ticket.
|
||||
*/
|
||||
public function messages()
|
||||
{
|
||||
return $this->hasMany(ChatMessage::class, 'ticket_id')->orderBy('created_at', 'asc');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
22
app/Providers/BroadcastServiceProvider.php
Normal file
22
app/Providers/BroadcastServiceProvider.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -4,4 +4,6 @@ return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\AuthServiceProvider::class,
|
||||
App\Providers\BroadcastServiceProvider::class,
|
||||
|
||||
];
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
"php": "^8.2",
|
||||
"barryvdh/laravel-dompdf": "^3.1",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/reverb": "^1.6",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"maatwebsite/excel": "^1.1",
|
||||
"maatwebsite/excel": "^3.1",
|
||||
"mpdf/mpdf": "^8.2",
|
||||
"php-open-source-saver/jwt-auth": "2.8",
|
||||
"spatie/laravel-permission": "^6.23"
|
||||
|
||||
1561
composer.lock
generated
1561
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -90,6 +90,8 @@ return [
|
||||
'model' => App\Models\Staff::class,
|
||||
],
|
||||
|
||||
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
31
config/broadcasting.php
Normal file
31
config/broadcasting.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'default' => env('BROADCAST_DRIVER', 'null'),
|
||||
|
||||
'connections' => [
|
||||
|
||||
'reverb' => [
|
||||
'driver' => 'reverb',
|
||||
'key' => env('REVERB_APP_KEY'),
|
||||
'secret' => env('REVERB_APP_SECRET'),
|
||||
'app_id' => env('REVERB_APP_ID'),
|
||||
'options' => [
|
||||
'host' => env('REVERB_HOST'),
|
||||
'port' => env('REVERB_PORT'),
|
||||
'scheme' => env('REVERB_SCHEME'),
|
||||
'useTLS' => false,
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
'log' => [
|
||||
'driver' => 'log',
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'null',
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -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),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
96
config/reverb.php
Normal file
96
config/reverb.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Reverb Server
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'default' => env('REVERB_SERVER', 'reverb'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reverb Servers
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'servers' => [
|
||||
|
||||
'reverb' => [
|
||||
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'), // WebSocket listens here
|
||||
'port' => env('REVERB_SERVER_PORT', 8080), // WebSocket port
|
||||
'path' => env('REVERB_SERVER_PATH', ''),
|
||||
|
||||
// Used for Echo client hostname
|
||||
'hostname' => env('REVERB_HOST', 'localhost'),
|
||||
|
||||
'options' => [
|
||||
'tls' => [], // No TLS for localhost
|
||||
],
|
||||
|
||||
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10000),
|
||||
|
||||
'scaling' => [
|
||||
'enabled' => env('REVERB_SCALING_ENABLED', false),
|
||||
],
|
||||
|
||||
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
|
||||
'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reverb Applications
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'apps' => [
|
||||
|
||||
'provider' => 'config',
|
||||
|
||||
'apps' => [
|
||||
[
|
||||
'key' => env('REVERB_APP_KEY'),
|
||||
'secret' => env('REVERB_APP_SECRET'),
|
||||
'app_id' => env('REVERB_APP_ID'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Echo + Flutter Client Options
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'options' => [
|
||||
'host' => env('REVERB_HOST', 'localhost'), // for client connections
|
||||
'port' => env('REVERB_PORT', 8080), // SAME as WebSocket server port
|
||||
'scheme' => env('REVERB_SCHEME', 'http'),
|
||||
'useTLS' => false,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Allowed Origins (Important)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| "*" allows all origins:
|
||||
| - Flutter (Android/iOS/Web)
|
||||
| - Admin Panel
|
||||
| - Localhost
|
||||
|
|
||||
*/
|
||||
'allowed_origins' => ['*'],
|
||||
|
||||
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
|
||||
'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
|
||||
'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'),
|
||||
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10000),
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -13,16 +13,28 @@ return new class extends Migration
|
||||
{
|
||||
Schema::create('chat_messages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('ticket_id'); // support ticket ID
|
||||
$table->unsignedBigInteger('sender_id'); // user or admin/staff
|
||||
$table->text('message')->nullable(); // message content
|
||||
$table->string('file_path')->nullable(); // image/pdf/video
|
||||
$table->string('file_type')->default('text'); // text/image/pdf/video
|
||||
|
||||
// Chat belongs to a ticket
|
||||
$table->unsignedBigInteger('ticket_id');
|
||||
|
||||
// POLYMORPHIC sender (User OR Admin)
|
||||
$table->unsignedBigInteger('sender_id');
|
||||
$table->string('sender_type');
|
||||
// Example values:
|
||||
// - "App\Models\User"
|
||||
// - "App\Models\Admin"
|
||||
|
||||
// Content
|
||||
$table->text('message')->nullable();
|
||||
$table->string('file_path')->nullable(); // storage/app/public/chat/...
|
||||
$table->string('file_type')->default('text'); // text / image / video / pdf
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// foreign keys
|
||||
$table->foreign('ticket_id')->references('id')->on('support_tickets')->onDelete('cascade');
|
||||
$table->foreign('sender_id')->references('id')->on('users')->onDelete('cascade'); // admin also stored in users table? If admin separate, change later.
|
||||
// FK to tickets table
|
||||
$table->foreign('ticket_id')
|
||||
->references('id')->on('support_tickets')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
//
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
2525
package-lock.json
generated
Normal file
2525
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,5 +13,9 @@
|
||||
"laravel-vite-plugin": "^2.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^7.0.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"laravel-echo": "^2.2.6",
|
||||
"pusher-js": "^8.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
BIN
public/profile_upload/profile_1766120292.jpg
Normal file
BIN
public/profile_upload/profile_1766120292.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@@ -1 +1,6 @@
|
||||
import './bootstrap';
|
||||
import "./bootstrap";
|
||||
|
||||
// VERY IMPORTANT — Load Echo globally
|
||||
import "./echo";
|
||||
|
||||
console.log("[APP] app.js loaded");
|
||||
|
||||
7
resources/js/bootstrap.js
vendored
7
resources/js/bootstrap.js
vendored
@@ -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;
|
||||
|
||||
|
||||
30
resources/js/echo.js
Normal file
30
resources/js/echo.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import Echo from "laravel-echo";
|
||||
import Pusher from "pusher-js";
|
||||
|
||||
window.Pusher = Pusher;
|
||||
|
||||
console.log("[ECHO] Initializing Reverb...");
|
||||
|
||||
window.Echo = new Echo({
|
||||
broadcaster: "reverb",
|
||||
key: import.meta.env.VITE_REVERB_APP_KEY,
|
||||
|
||||
wsHost: import.meta.env.VITE_REVERB_HOST,
|
||||
wsPort: Number(import.meta.env.VITE_REVERB_PORT),
|
||||
|
||||
forceTLS: false,
|
||||
disableStats: true,
|
||||
|
||||
authEndpoint: "/broadcasting/auth",
|
||||
|
||||
auth: {
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": document
|
||||
.querySelector('meta[name="csrf-token"]')
|
||||
?.getAttribute("content"),
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log("[ECHO] Loaded Successfully!", window.Echo);
|
||||
@@ -48,7 +48,7 @@ body {
|
||||
|
||||
/* top actions row */
|
||||
.top-actions {
|
||||
display:flex; align-items:center; justify-content:space-between;
|
||||
align-items:center; justify-content:space-between;
|
||||
gap:12px; margin:16px 0 20px 0; flex-wrap:wrap;
|
||||
}
|
||||
.top-actions .left {
|
||||
@@ -67,7 +67,7 @@ body {
|
||||
cursor:pointer; transition: transform .15s ease, box-shadow .15s;
|
||||
}
|
||||
.btn.ghost { background: transparent; color:var(--primary-1); border:1.5px solid #dbe4f5; box-shadow:none; }
|
||||
.btn:hover{ transform: translateY(-3px); box-shadow: 0 8px 26px rgba(36,58,114,0.12); }
|
||||
.btn:hover{ transform: translateY(-3px); box-shadow: 0 8px 26px rgba(227, 229, 234, 0.12); }
|
||||
|
||||
/* account panels */
|
||||
.account-panels {
|
||||
@@ -89,12 +89,12 @@ body {
|
||||
background: var(--card-bg);
|
||||
border-radius:12px;
|
||||
box-shadow:0 8px 20px rgba(25,40,80,0.06);
|
||||
padding:22px;
|
||||
padding:20px; /* 005 */
|
||||
box-sizing:border-box;
|
||||
overflow-x:auto;
|
||||
transition: transform .12s, box-shadow .12s;
|
||||
min-height: 520px;
|
||||
display: flex;
|
||||
/* display: flex; */ /* 005 */
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
@@ -205,8 +205,8 @@ tr:hover td{ background:#fbfdff; }
|
||||
.toggle-switch-btn {
|
||||
appearance:none;
|
||||
-webkit-appearance:none;
|
||||
width:60px;
|
||||
height:24px;
|
||||
width:64px;
|
||||
height:26.5px; /* 005 */
|
||||
background:#f25b5b;
|
||||
border:2px solid #f25b5b;
|
||||
border-radius:999px;
|
||||
@@ -313,7 +313,7 @@ tr:hover td{ background:#fbfdff; }
|
||||
margin-top: 15px;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid #eef3fb;
|
||||
margin-right:550px;
|
||||
/* margin-right:550px; */
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
@@ -326,14 +326,15 @@ tr:hover td{ background:#fbfdff; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-right:-1050px;
|
||||
position: absolute;
|
||||
right: 16px; /* 005 */
|
||||
|
||||
}
|
||||
.pagination-controls1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-right:-550px;
|
||||
/* margin-right:-550px; */
|
||||
|
||||
}
|
||||
|
||||
@@ -563,7 +564,7 @@ tr:hover td{ background:#fbfdff; }
|
||||
|
||||
/* Combined filters row styling */
|
||||
.combined-filters-row {
|
||||
display: flex;
|
||||
display: ruby; /* 005 */
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
@@ -576,8 +577,8 @@ tr:hover td{ background:#fbfdff; }
|
||||
}
|
||||
|
||||
.right{
|
||||
margin-left:auto;
|
||||
margin-top:-16px;
|
||||
/* margin-left:auto;
|
||||
margin-top:-16px; */ /* 005 */
|
||||
}
|
||||
|
||||
.filter-group1 {
|
||||
@@ -953,6 +954,15 @@ tr:hover td{ background:#fbfdff; }
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.combined-top-row .btn:hover {
|
||||
color: #ffffff !important;
|
||||
background-color: inherit !important;
|
||||
border-color: inherit !important;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
|
||||
.remove-order-btn:hover {
|
||||
background: #d42c3f;
|
||||
}
|
||||
@@ -1021,6 +1031,153 @@ tr:hover td{ background:#fbfdff; }
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
/* नवीन responsive styles */
|
||||
@media (max-width:980px){
|
||||
.account-container {
|
||||
padding: 20px !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.panel-card {
|
||||
min-width:100% !important;
|
||||
padding: 18px !important;
|
||||
}
|
||||
|
||||
.search-row input{
|
||||
width:220px !important;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.combined-filters-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filter-group1,
|
||||
.filter-group2,
|
||||
.filter-group3,
|
||||
.filter-group4 {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:768px){
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.account-container {
|
||||
padding: 15px !important;
|
||||
}
|
||||
|
||||
.account-header {
|
||||
padding: 18px !important;
|
||||
}
|
||||
|
||||
.top-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.top-actions .left {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.account-panels {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.search-row input {
|
||||
width: 100% !important;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
/* pagination adjustments for mobile */
|
||||
.pagination-controls,
|
||||
.pagination-controls1,
|
||||
.pagination-controls2 {
|
||||
margin-right: 0 !important;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Zoom out specific fix */
|
||||
@media screen and (max-width: 1200px) {
|
||||
.account-container {
|
||||
padding: 20px;
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.payment-block {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Extra small screens */
|
||||
@media screen and (max-width: 576px) {
|
||||
.account-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.account-header h2 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.panel-card {
|
||||
padding: 15px;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Prevent horizontal scroll on very small screens */
|
||||
@media screen and (max-width: 400px) {
|
||||
body {
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
.account-container {
|
||||
padding: 10px 5px;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fix for sidebar gap on zoom out */
|
||||
html, body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Ensure all containers are properly constrained */
|
||||
.container, .container-fluid, .account-container {
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<div class="account-container">
|
||||
@@ -1167,7 +1324,7 @@ tr:hover td{ background:#fbfdff; }
|
||||
<div class="create-order-modal" id="createOrderModal">
|
||||
<div class="modal-box">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:16px;">
|
||||
<div style="font-size:20px; font-weight:800; color:var(--primary-1)">Create New Installment</div>
|
||||
<div style="font-size:20px; font-weight:800;">Create New Installment</div>
|
||||
<button class="btn ghost" id="closeCreateModal" title="Close create form">✕</button>
|
||||
</div>
|
||||
|
||||
@@ -2627,11 +2784,9 @@ async function submitEditEntry(e) {
|
||||
}
|
||||
|
||||
|
||||
function openEntryOrdersModal(entryNo) {
|
||||
// header la entry no show kar
|
||||
function openEntryOrdersModal(entryNo) {
|
||||
document.getElementById('entryOrdersEntryNo-span').textContent = `(${entryNo})`;
|
||||
|
||||
// table clean / loading state
|
||||
const tbody = document.getElementById('entryOrdersTableBody');
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
@@ -2639,7 +2794,6 @@ function openEntryOrdersModal(entryNo) {
|
||||
</tr>
|
||||
`;
|
||||
|
||||
// API call: /admin/account/entry-orders/{entryno}
|
||||
jsonFetch(`/admin/account/entry-orders/${encodeURIComponent(entryNo)}`, {
|
||||
method: 'GET'
|
||||
})
|
||||
@@ -2664,16 +2818,10 @@ function openEntryOrdersModal(entryNo) {
|
||||
}
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
orders.forEach(order => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
const idString = (order.orderid ?? order.id ?? '').toString().trim();
|
||||
const numericId = parseInt(idString, 10);
|
||||
const formattedId = isNaN(numericId)
|
||||
? escapeHtml(idString)
|
||||
: 'KNT-25-' + String(numericId).padStart(8, '0');
|
||||
|
||||
// इथे वेगवेगळी शक्य keys try कर
|
||||
const amountValue =
|
||||
order.ttl_amount ??
|
||||
order.ttlamount ??
|
||||
@@ -2683,18 +2831,17 @@ function openEntryOrdersModal(entryNo) {
|
||||
0;
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>${formattedId}</td>
|
||||
<td>${escapeHtml(order.markno ?? order.mark_no ?? '')}</td>
|
||||
<td>${escapeHtml(order.order_id)}</td>
|
||||
<td>${escapeHtml(order.mark_no ?? '')}</td>
|
||||
<td>${escapeHtml(order.origin ?? '')}</td>
|
||||
<td>${escapeHtml(order.destination ?? '')}</td>
|
||||
<td>${escapeHtml(order.ctn ?? '')}</td>
|
||||
<td>${escapeHtml(order.qty ?? '')}</td>
|
||||
<td>${formatCurrency(amountValue)}</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
tbody.innerHTML = `
|
||||
@@ -2705,7 +2852,8 @@ function openEntryOrdersModal(entryNo) {
|
||||
});
|
||||
|
||||
document.getElementById('entryOrdersModal').classList.add('modal-open');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function closeEntryOrdersModal() {
|
||||
document.getElementById('entryOrdersModal').classList.remove('modal-open');
|
||||
|
||||
@@ -1,12 +1,621 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Dashboard')
|
||||
@section('page-title', 'Chat Support Dashboard')
|
||||
|
||||
@section('content')
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h4>Welcome to the Admin chat</h4>
|
||||
<p>Here you can manage all system modules.</p>
|
||||
<style>
|
||||
:root {
|
||||
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--success-gradient: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
--danger-gradient: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%);
|
||||
--warning-gradient: linear-gradient(135deg, #f7971e 0%, #ffd200 100%);
|
||||
--info-gradient: linear-gradient(135deg, #56ccf2 0%, #2f80ed 100%);
|
||||
--card-shadow: 0 3px 10px rgba(0,0,0,0.05);
|
||||
--card-shadow-hover: 0 6px 16px rgba(0,0,0,0.10);
|
||||
--border-radius: 8px;
|
||||
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chat-dashboard {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 0.75rem;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: clamp(1.4rem, 2.5vw, 2rem);
|
||||
font-weight: 800;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin: 0 0 0.4rem 0;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dashboard-title::before {
|
||||
content: '💬';
|
||||
position: absolute;
|
||||
left: -1.6rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 1.3rem;
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% { transform: translateY(-50%) translateY(0); }
|
||||
40% { transform: translateY(-50%) translateY(-5px); }
|
||||
60% { transform: translateY(-50%) translateY(-2px); }
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
color: #64748b;
|
||||
font-size: 0.8rem;
|
||||
max-width: 380px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* 🔔 GLOBAL NEW MESSAGE COUNTER */
|
||||
.global-notify {
|
||||
margin: 0 auto 0.75rem auto;
|
||||
max-width: 320px;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(15,23,42,0.03);
|
||||
border: 1px dashed #cbd5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.78rem;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.global-notify-badge {
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
min-width: 18px;
|
||||
padding: 0 0.35rem;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 0 0 2px rgba(254, 226, 226, 0.8);
|
||||
}
|
||||
|
||||
.global-notify.d-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tickets-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.75rem 0.9rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
/* max-height / overflow काढले, जेणेकरून बाहेरचा page scroll वापरला जाईल */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tickets-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.6rem;
|
||||
padding-bottom: 0.45rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.tickets-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.tickets-count {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ticket-item {
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.6rem;
|
||||
margin-bottom: 0.3rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: var(--primary-gradient);
|
||||
transform: scaleY(0);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.ticket-item:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--card-shadow-hover);
|
||||
border-color: rgba(102, 126, 234, 0.35);
|
||||
}
|
||||
|
||||
.ticket-item:hover::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.ticket-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.ticket-avatar {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 6px;
|
||||
background: var(--info-gradient);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 3px 8px rgba(86, 204, 242, 0.25);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-avatar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
transition: left 0.4s;
|
||||
}
|
||||
|
||||
.ticket-item:hover .ticket-avatar::after {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.ticket-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ticket-name {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 0 0 0.08rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.unread-count {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 6px rgba(239, 68, 68, 0.4);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.ticket-preview {
|
||||
color: #64748b;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.25;
|
||||
margin-bottom: 0.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
max-height: 1.8em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-time {
|
||||
font-size: 0.65rem;
|
||||
color: #94a3b8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
.ticket-time svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ticket-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 0.3rem;
|
||||
border-top: 1px dashed #e5e7eb;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.14rem 0.45rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.status-open { background: var(--success-gradient); color: white; }
|
||||
.status-closed{ background: var(--danger-gradient); color: white; }
|
||||
|
||||
.chat-btn {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
font-size: 0.7rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.25);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.35);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat-btn::after {
|
||||
content: '→';
|
||||
transition: margin-left 0.25s ease;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.chat-btn:hover::after {
|
||||
margin-left: 0.18rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 1.6rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: var(--border-radius);
|
||||
border: 2px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 2.8rem;
|
||||
margin-bottom: 0.9rem;
|
||||
display: block;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.empty-subtitle {
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.ticket-id {
|
||||
background: rgba(102, 126, 234, 0.08);
|
||||
color: #667eea;
|
||||
padding: 0.12rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
.new-message-dot {
|
||||
position: absolute;
|
||||
top: 0.45rem;
|
||||
right: 0.5rem;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: #ef4444;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.3);
|
||||
animation: blink 1.5s infinite;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(1.1); }
|
||||
}
|
||||
|
||||
/* इथे आता inner scroll नाही */
|
||||
.tickets-list {
|
||||
/* flex: 1; काढला, overflow काढला, parent + body scroll वापरेल */
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar-thumb {
|
||||
background: var(--primary-gradient);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chat-dashboard {
|
||||
padding: 0.6rem;
|
||||
}
|
||||
|
||||
.tickets-container {
|
||||
/* max-height काढलेले, mobile वरही outer scroll */
|
||||
}
|
||||
|
||||
.ticket-header {
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.ticket-footer {
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.chat-btn {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="chat-dashboard">
|
||||
<!-- Header -->
|
||||
<div class="dashboard-header">
|
||||
<h1 class="dashboard-title">Live Chat Dashboard</h1>
|
||||
<p class="dashboard-subtitle">
|
||||
Monitor customer conversations with real-time updates
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 🔔 GLOBAL NEW MESSAGES NOTIFICATION -->
|
||||
<div id="globalNewMessageBox" class="global-notify d-none">
|
||||
<span>New messages:</span>
|
||||
<span id="globalNewMessageCount" class="global-notify-badge">0</span>
|
||||
</div>
|
||||
|
||||
<!-- Tickets Container -->
|
||||
<div class="tickets-container">
|
||||
<div class="tickets-header">
|
||||
<div>
|
||||
<h2 class="tickets-title">
|
||||
📋 Active Conversations
|
||||
<span class="tickets-count">{{ $tickets->count() }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($tickets->count() === 0)
|
||||
<!-- Empty State -->
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">💬</div>
|
||||
<h3 class="empty-title">No Active Conversations</h3>
|
||||
<p class="empty-subtitle">
|
||||
Customer conversations will appear here with real-time notifications.
|
||||
</p>
|
||||
<div class="ticket-id">Ready for support requests</div>
|
||||
</div>
|
||||
@else
|
||||
<!-- Tickets List -->
|
||||
<div class="tickets-list">
|
||||
@foreach($tickets as $ticket)
|
||||
<div class="ticket-item" data-ticket-id="{{ $ticket->id }}">
|
||||
@if($ticket->unread_count > 0)
|
||||
<div class="new-message-dot"></div>
|
||||
@endif
|
||||
|
||||
<div class="ticket-header">
|
||||
<div class="ticket-avatar">
|
||||
{{ strtoupper(substr($ticket->user->customer_name ?? $ticket->user->name, 0, 1)) }}
|
||||
</div>
|
||||
<div class="ticket-content">
|
||||
<div class="ticket-name">
|
||||
{{ $ticket->user->customer_name ?? $ticket->user->name }}
|
||||
@if($ticket->unread_count > 0)
|
||||
<span id="badge-{{ $ticket->id }}" class="unread-count">
|
||||
{{ $ticket->unread_count }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@php
|
||||
$lastMsg = $ticket->messages()->latest()->first();
|
||||
@endphp
|
||||
|
||||
<div class="ticket-preview">
|
||||
@if($lastMsg)
|
||||
@if($lastMsg->message)
|
||||
{{ Str::limit($lastMsg->message, 45) }}
|
||||
@elseif(Str::startsWith($lastMsg->file_type, 'image'))
|
||||
📷 Photo shared
|
||||
@else
|
||||
📎 File attached
|
||||
@endif
|
||||
@else
|
||||
<em>Conversation started</em>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($lastMsg)
|
||||
<div class="ticket-time">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
|
||||
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
|
||||
</svg>
|
||||
{{ $lastMsg->created_at->diffForHumans() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ticket-footer">
|
||||
<span class="status-badge status-{{ $ticket->status }}">
|
||||
{{ ucfirst($ticket->status) }}
|
||||
</span>
|
||||
<a href="{{ route('admin.chat.open', $ticket->id) }}" class="chat-btn">
|
||||
Open Chat
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</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
|
||||
|
||||
1065
resources/views/admin/chat_window.blade.php
Normal file
1065
resources/views/admin/chat_window.blade.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
||||
@extends('admin.layouts.app')
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Customers')
|
||||
@section('page-title', 'Customers')
|
||||
|
||||
@section('content')
|
||||
@section('content')
|
||||
|
||||
<style>
|
||||
<style>
|
||||
/* Import Inter font */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
@@ -700,9 +700,9 @@
|
||||
padding: 2px 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="container-fluid">
|
||||
<!-- Header - Removed gradient -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 style="color: #2c3e50; font-weight: 700; font-family: 'Inter', sans-serif;">Customer List</h4>
|
||||
@@ -827,14 +827,30 @@
|
||||
<th class="table-header">Customer Info</th>
|
||||
<th class="table-header">Customer ID</th>
|
||||
<th class="table-header">Orders</th>
|
||||
<th class="table-header">Total</th>
|
||||
<th class="table-header">Order Total</th>
|
||||
<th class="table-header">Total Payable</th> {{-- NEW --}}
|
||||
<th class="table-header">Remaining</th> {{-- NEW --}}
|
||||
<th class="table-header">Paid Amount</th>
|
||||
<th class="table-header">Create Date</th>
|
||||
<th class="table-header">Status</th>
|
||||
<th class="table-header" width="100">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody id="customersTableBody">
|
||||
@forelse($customers as $c)
|
||||
@php
|
||||
// Invoice total (with GST)
|
||||
$totalPayable = $c->invoices->sum('final_amount_with_gst');
|
||||
|
||||
// Total paid via installments
|
||||
$totalPaid = $c->invoices
|
||||
->flatMap(fn($inv) => $inv->installments)
|
||||
->sum('amount');
|
||||
|
||||
// Remaining amount
|
||||
$remainingAmount = max($totalPayable - $totalPaid, 0);
|
||||
@endphp
|
||||
<tr>
|
||||
<!-- Customer Info Column -->
|
||||
<td class="customer-info-column">
|
||||
@@ -869,9 +885,36 @@
|
||||
|
||||
<!-- Total Column -->
|
||||
<td class="total-column">
|
||||
<span class="total-amount">₹{{ number_format($c->orders->sum('ttl_amount'), 2) }}</span>
|
||||
<span class="total-amount">
|
||||
₹{{ number_format($c->orders->sum('ttl_amount'), 2) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="total-column">
|
||||
<span class="total-amount">
|
||||
₹{{ number_format($totalPayable, 2) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="total-column">
|
||||
@if($remainingAmount > 0)
|
||||
<span class="text-danger fw-bold">
|
||||
₹{{ number_format($remainingAmount, 2) }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-success fw-bold">
|
||||
₹0.00
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<td class="total-column">
|
||||
<span class="fw-bold text-success">
|
||||
₹{{ number_format($totalPaid, 2) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
|
||||
<!-- Create Date -->
|
||||
<td class="create-date-column">
|
||||
<span class="text-muted">{{ $c->created_at ? $c->created_at->format('d-m-Y') : '-' }}</span>
|
||||
@@ -945,9 +988,9 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add hover effects to table rows
|
||||
const tableRows = document.querySelectorAll('.table tbody tr');
|
||||
@@ -974,6 +1017,6 @@
|
||||
@endif
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
@endsection
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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">
|
||||
|
||||
@@ -1,7 +1,30 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Dashboard')
|
||||
@php
|
||||
use App\Models\Order;
|
||||
use App\Models\OrderItem;
|
||||
use App\Models\Shipment;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\User;
|
||||
use App\Models\Admin;
|
||||
|
||||
$totalOrders = Order::count();
|
||||
$pendingOrders = Order::where('status', 'pending')->count();
|
||||
$totalShipments = Shipment::count();
|
||||
$totalItems = OrderItem::count();
|
||||
|
||||
$totalRevenue = Invoice::sum('final_amount_with_gst');
|
||||
|
||||
// USERS (CUSTOMERS)
|
||||
$activeCustomers = User::where('status', 'active')->count();
|
||||
$inactiveCustomers = User::where('status', 'inactive')->count();
|
||||
|
||||
// STAFF (FROM ADMINS TABLE)
|
||||
$totalStaff = Admin::where('type', 'staff')->count();
|
||||
|
||||
$orders = Order::latest()->get();
|
||||
@endphp
|
||||
@section('content')
|
||||
<style>
|
||||
/* ===== GLOBAL RESPONSIVE STYLES ===== */
|
||||
@@ -238,7 +261,6 @@ body, .container-fluid {
|
||||
font-family: 'Inter', sans-serif;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
.table thead th:first-child { border-radius: 9px 0 0 0;}
|
||||
.table thead th:last-child { border-radius: 0 9px 0 0;}
|
||||
|
||||
@@ -333,6 +355,13 @@ body, .container-fluid {
|
||||
border-color: #f59e0b !important;
|
||||
}
|
||||
|
||||
.badge-loading {
|
||||
background: linear-gradient(135deg, #e3f2fd, #90caf9) !important;
|
||||
color: #1565c0 !important;
|
||||
border-color: #2196f3 !important;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
/* In Transit Status - SAME SIZE WITH TRUCK ICON */
|
||||
.badge-in_transit {
|
||||
background: linear-gradient(135deg, #dbeafe, #93c5fd) !important;
|
||||
@@ -436,45 +465,47 @@ body, .container-fluid {
|
||||
min-width: 2000px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* ===== CREATE ORDER MODAL STYLES ===== */
|
||||
.create-order-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
width: 100vw; /* full screen */
|
||||
height: 100vh; /* full screen */
|
||||
background: #f8fafc; /* backdrop काढून direct page सारखा bg */
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: flex-start; /* top पासून सुरू कर */
|
||||
align-items: stretch; /* full height */
|
||||
z-index: 9999;
|
||||
overflow-y: auto; /* content scroll */
|
||||
}
|
||||
|
||||
/* JS madhun modal.classList.add('show') already aahe */
|
||||
.create-order-modal.show {
|
||||
display: flex;
|
||||
display: block; /* flex ऐवजी block => full page section सारखा */
|
||||
}
|
||||
|
||||
/* Card ला full-width/height सारखं कर */
|
||||
.create-order-modal .modal-card {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3);
|
||||
width: 95%;
|
||||
max-width: 900px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
border-radius: 0; /* corner radius काढला => normal page feel */
|
||||
box-shadow: none; /* popup सारखा shadow काढला */
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 100vh;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.create-order-modal .modal-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px 25px;
|
||||
border-radius: 16px 16px 0 0;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.create-order-modal .modal-title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
@@ -600,6 +631,13 @@ body, .container-fluid {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* items table inputs */
|
||||
.items-input {
|
||||
width: 100%;
|
||||
padding: 4px 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ===== ORDER DETAILS MODAL STYLES ===== */
|
||||
.modal.fade .modal-dialog {
|
||||
transition: transform 0.3s ease-out;
|
||||
@@ -839,6 +877,27 @@ body, .container-fluid {
|
||||
.pagination-controls {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.create-order-modal .modal-card {
|
||||
width: 98%;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.create-order-modal .modal-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.create-order-modal .modal-header {
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px 15px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile Landscape (768px and below) */
|
||||
@@ -1101,6 +1160,101 @@ body, .container-fluid {
|
||||
break-inside: avoid;
|
||||
}
|
||||
}
|
||||
/* =====================================================
|
||||
GLOBAL EDGE-TO-EDGE + ZOOM SAFE PATCH (CSS ONLY)
|
||||
===================================================== */
|
||||
|
||||
/* 1️⃣ Kill boxed layouts on desktop & zoom */
|
||||
html, body {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
/* 2️⃣ Force container-fluid to truly span full width */
|
||||
.container-fluid {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding-left: clamp(12px, 1.8vw, 28px) !important;
|
||||
padding-right: clamp(12px, 1.8vw, 28px) !important;
|
||||
}
|
||||
|
||||
/* 3️⃣ Zoom-safe scaling (VERY IMPORTANT) */
|
||||
body {
|
||||
font-size: clamp(14px, 0.95vw, 16px);
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
DASHBOARD CARD & GRID FIXES (NO HTML CHANGE)
|
||||
===================================================== */
|
||||
|
||||
/* 4️⃣ Make stat grids auto-adjust on zoom */
|
||||
.stats-row,
|
||||
.shipment-totals-row {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)) !important;
|
||||
gap: clamp(12px, 1.5vw, 20px) !important;
|
||||
}
|
||||
|
||||
/* 5️⃣ Prevent hover zoom breaking layout */
|
||||
.stats-card:hover,
|
||||
.card:hover,
|
||||
.table tbody tr:hover {
|
||||
transform: translateY(-4px) !important;
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
TABLE ZOOM FIX (NO MORE CRUSHING / OVERFLOW)
|
||||
===================================================== */
|
||||
|
||||
/* 6️⃣ Tables behave like shipment page */
|
||||
.table-responsive {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* 7️⃣ Remove hard min-widths that break zoom */
|
||||
.table,
|
||||
.custom-table-modal,
|
||||
.shipment-details-table {
|
||||
width: 100% !important;
|
||||
min-width: max-content !important;
|
||||
}
|
||||
|
||||
/* 8️⃣ Let text wrap naturally when zoomed */
|
||||
.table td,
|
||||
.table th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
MODALS – EDGE TO EDGE WITHOUT TOUCHING MARKUP
|
||||
===================================================== */
|
||||
|
||||
.modal-xl {
|
||||
max-width: 96vw !important;
|
||||
width: 96vw !important;
|
||||
margin: 1vh auto !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.modal-xl {
|
||||
max-width: 100vw !important;
|
||||
width: 100vw !important;
|
||||
margin: 0 !important;
|
||||
height: 100vh !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
FINAL SAFETY – PREVENT LAYOUT SHRINK ON ZOOM
|
||||
===================================================== */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class="container-fluid py-3">
|
||||
@@ -1114,16 +1268,16 @@ body, .container-fluid {
|
||||
<!-- STATS CARDS -->
|
||||
<div class="stats-row-wrap">
|
||||
<div class="stats-row">
|
||||
<div class="stats-card stats-card-blue"><span class="stats-icon">📦</span><div class="stats-label">Total Shipments</div><div class="stats-value">1,247</div></div>
|
||||
<div class="stats-card stats-card-blue"><span class="stats-icon">👥</span><div class="stats-label">Active Customers</div><div class="stats-value">342</div></div>
|
||||
<div class="stats-card stats-card-green"><span class="stats-icon">💰</span><div class="stats-label">Total Revenue</div><div class="stats-value">₹123</div></div>
|
||||
<div class="stats-card stats-card-red"><span class="stats-icon">⏳</span><div class="stats-label">Pending Order</div><div class="stats-value">23</div></div>
|
||||
<div class="stats-card stats-card-blue"><span class="stats-icon">📦</span><div class="stats-label">Total Shipments</div><div class="stats-value">{{ $totalShipments }}</div></div>
|
||||
<div class="stats-card stats-card-blue"><span class="stats-icon">👥</span><div class="stats-label">Active Customers</div><div class="stats-value">{{ $activeCustomers }}</div></div>
|
||||
<div class="stats-card stats-card-green"><span class="stats-icon">💰</span><div class="stats-label">Total Revenue</div><div class="stats-value">₹{{ number_format($totalRevenue, 2) }}</div></div>
|
||||
<div class="stats-card stats-card-red"><span class="stats-icon">⏳</span><div class="stats-label">Pending Order</div><div class="stats-value">{{ $pendingOrders }}</div></div>
|
||||
</div>
|
||||
<div class="stats-row">
|
||||
<div class="stats-card stats-card-blue"><span class="stats-icon">📦</span><div class="stats-label">Total Orders</div><div class="stats-value">453</div></div>
|
||||
<div class="stats-card stats-card-blue"><span class="stats-icon">🧑💼</span><div class="stats-label">Total Staff</div><div class="stats-value">125</div></div>
|
||||
<div class="stats-card stats-card-blue"><span class="stats-icon">📦</span><div class="stats-label">Total Items</div><div class="stats-value">321</div></div>
|
||||
<div class="stats-card stats-card-orng"><span class="stats-icon">⛔</span><div class="stats-label">Inactive Customers</div><div class="stats-value">10</div></div>
|
||||
<div class="stats-card stats-card-blue"><span class="stats-icon">📦</span><div class="stats-label">Total Orders</div><div class="stats-value">{{ $totalOrders }}</div></div>
|
||||
<div class="stats-card stats-card-blue"><span class="stats-icon">🧑💼</span><div class="stats-label">Total Staff</div><div class="stats-value">{{ $totalStaff }}</div></div>
|
||||
<div class="stats-card stats-card-blue"><span class="stats-icon">📦</span><div class="stats-label">Total Items</div><div class="stats-value">{{ $totalItems }}</div></div>
|
||||
<div class="stats-card stats-card-orng"><span class="stats-icon">⛔</span><div class="stats-label">Inactive Customers</div><div class="stats-value">{{ $inactiveCustomers }}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1138,7 +1292,6 @@ body, .container-fluid {
|
||||
@endcan
|
||||
|
||||
</div>
|
||||
|
||||
<div class="order-mgmt-main">
|
||||
<!-- RECENT ORDERS TABLE -->
|
||||
<div class="card shadow-sm">
|
||||
@@ -1156,6 +1309,7 @@ body, .container-fluid {
|
||||
<th>#</th>
|
||||
<th>Order ID</th>
|
||||
<th>Mark No</th>
|
||||
<th>Date</th>
|
||||
<th>Origin</th>
|
||||
<th>Destination</th>
|
||||
<th>Total CTN</th>
|
||||
@@ -1167,7 +1321,6 @@ body, .container-fluid {
|
||||
<th>Total KG</th>
|
||||
<th>Total TTL KG</th>
|
||||
<th>Status</th>
|
||||
<th>Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -1195,19 +1348,49 @@ body, .container-fluid {
|
||||
<td>{{ $order->kg }}</td>
|
||||
<td>{{ $order->ttl_kg }}</td>
|
||||
<td>
|
||||
<span class="badge badge-{{ $order->status }}">
|
||||
@if($order->status == 'pending')
|
||||
<i class="bi bi-clock-fill status-icon"></i>
|
||||
@elseif($order->status == 'in_transit')
|
||||
<i class="bi bi-truck status-icon"></i>
|
||||
@elseif($order->status == 'dispatched')
|
||||
<i class="bi bi-box-seam status-icon"></i>
|
||||
@elseif($order->status == 'delivered')
|
||||
<i class="bi bi-check-circle-fill status-icon"></i>
|
||||
@endif
|
||||
{{ ucfirst(str_replace('_', ' ', $order->status)) }}
|
||||
@php
|
||||
// Badge color mapping
|
||||
$badgeMap = [
|
||||
'order_placed' => 'secondary',
|
||||
'order_confirmed' => 'info',
|
||||
'supplier_warehouse' => 'warning',
|
||||
'consolidate_warehouse' => 'warning',
|
||||
'export_custom' => 'primary',
|
||||
'international_transit' => 'primary',
|
||||
'arrived_india' => 'info',
|
||||
'import_custom' => 'info',
|
||||
'warehouse' => 'dark',
|
||||
'domestic_distribution' => 'primary',
|
||||
'out_for_delivery' => 'success',
|
||||
'delivered' => 'success',
|
||||
];
|
||||
|
||||
// Icon mapping
|
||||
$iconMap = [
|
||||
'order_placed' => 'bi-clock-fill',
|
||||
'order_confirmed' => 'bi-check-circle',
|
||||
'supplier_warehouse' => 'bi-box-seam',
|
||||
'consolidate_warehouse' => 'bi-boxes',
|
||||
'export_custom' => 'bi-upload',
|
||||
'international_transit' => 'bi-truck',
|
||||
'arrived_india' => 'bi-geo-alt',
|
||||
'import_custom' => 'bi-download',
|
||||
'warehouse' => 'bi-building',
|
||||
'domestic_distribution' => 'bi-diagram-3',
|
||||
'out_for_delivery' => 'bi-truck-flatbed',
|
||||
'delivered' => 'bi-check-circle-fill',
|
||||
];
|
||||
|
||||
$badgeClass = $badgeMap[$order->status] ?? 'secondary';
|
||||
$iconClass = $iconMap[$order->status] ?? 'bi-info-circle';
|
||||
@endphp
|
||||
|
||||
<span class="badge bg-{{ $badgeClass }}">
|
||||
<i class="bi {{ $iconClass }} status-icon"></i>
|
||||
{{ $order->status_label }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>{{ $order->created_at->format('d-m-Y') }}</td>
|
||||
<td>
|
||||
<a href="{{ route('admin.orders.show', $order->id) }}" class="btn btn-sm btn-outline-primary">
|
||||
@@ -1317,22 +1500,35 @@ body, .container-fluid {
|
||||
|
||||
{{-- ITEM INPUTS --}}
|
||||
<h6 class="text-primary">Add Item</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Description</label>
|
||||
<input type="text" class="form-control" name="description" id="itemDescription" required placeholder="Enter item description">
|
||||
|
||||
{{-- NEW ITEMS TABLE (INSTEAD OF SINGLE-ROW INPUTS) --}}
|
||||
<div class="table-wrapper mb-3">
|
||||
<table class="table table-bordered table-sm align-middle text-center" id="itemsTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Description</th>
|
||||
<th>CTN</th>
|
||||
<th>QTY</th>
|
||||
<th>TTL/QTY</th>
|
||||
<th>Unit</th>
|
||||
<th>Price</th>
|
||||
<th>TTL Amount</th>
|
||||
<th>CBM</th>
|
||||
<th>TTL CBM</th>
|
||||
<th>KG</th>
|
||||
<th>TTL KG</th>
|
||||
<th>Shop No</th>
|
||||
<th>Remove</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="itemsTableBody">
|
||||
{{-- JS will create default 2 blank rows --}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-2"><label class="form-label">CTN</label><input type="number" name="ctn" id="itemCtn" class="form-control" placeholder="CTN"></div>
|
||||
<div class="col-md-2"><label class="form-label">QTY</label><input type="number" name="qty" id="itemQty" class="form-control" placeholder="QTY"></div>
|
||||
<div class="col-md-2"><label class="form-label">TTL/QTY</label><input type="number" name="ttl_qty" id="itemTtlQty" class="form-control" placeholder="TTL/QTY"></div>
|
||||
<div class="col-md-2"><label class="form-label">Unit</label><input type="text" name="unit" id="itemUnit" class="form-control" placeholder="Unit"></div>
|
||||
<div class="col-md-2"><label class="form-label">Price</label><input type="number" step="0.01" name="price" id="itemPrice" class="form-control" placeholder="Price"></div>
|
||||
<div class="col-md-2"><label class="form-label">TTL Amount</label><input type="number" step="0.01" name="ttl_amount" id="itemTtlAmount" class="form-control" placeholder="TTL Amount"></div>
|
||||
<div class="col-md-2"><label class="form-label">CBM</label><input type="number" step="0.001" name="cbm" id="itemCbm" class="form-control" placeholder="CBM"></div>
|
||||
<div class="col-md-2"><label class="form-label">TTL CBM</label><input type="number" step="0.001" name="ttl_cbm" id="itemTtlCbm" class="form-control" placeholder="TTL CBM"></div>
|
||||
<div class="col-md-2"><label class="form-label">KG</label><input type="number" step="0.001" name="kg" id="itemKg" class="form-control" placeholder="KG"></div>
|
||||
<div class="col-md-2"><label class="form-label">TTL KG</label><input type="number" step="0.001" name="ttl_kg" id="itemTtlKg" class="form-control" placeholder="TTL KG"></div>
|
||||
<div class="col-md-3"><label class="form-label">Shop No</label><input type="text" name="shop_no" id="itemShopNo" class="form-control" placeholder="Shop No"></div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12 text-end mt-3">
|
||||
<button type="button" class="btn btn-secondary clear-form-btn" id="clearForm">
|
||||
<i class="bi bi-arrow-clockwise"></i> Clear Form
|
||||
@@ -1340,6 +1536,13 @@ body, .container-fluid {
|
||||
<button type="submit" class="btn btn-info" id="addItemBtn">
|
||||
<i class="bi bi-plus-circle"></i> Add Item
|
||||
</button>
|
||||
|
||||
<input type="file" id="excelInput" accept=".xlsx,.xls" hidden>
|
||||
|
||||
<button type="button" class="btn btn-outline-success" id="uploadExcelBtn">
|
||||
<i class="bi bi-file-earmark-excel"></i> Upload Excel
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1454,7 +1657,134 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const ordersPerPage = 10;
|
||||
let allOrders = @json($orders->values());
|
||||
|
||||
// Initialize pagination
|
||||
// ------- ITEMS TABLE LOGIC (NEW) -------
|
||||
|
||||
const itemsTableBody = document.getElementById('itemsTableBody');
|
||||
|
||||
function addRow(index) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td class="align-middle fw-bold">${index + 1}</td>
|
||||
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm items-input"name="items[${index}][description]"data-field="description">
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm items-input"name="items[${index}][ctn]"data-field="ctn">
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm items-input"name="items[${index}][qty]"data-field="qty">
|
||||
</td>
|
||||
|
||||
<!-- 🔒 AUTO: TTL/QTY = CTN * QTY -->
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm bg-light"name="items[${index}][ttl_qty]"data-field="ttl_qty"readonly>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm items-input"name="items[${index}][unit]"data-field="unit">
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm items-input"name="items[${index}][price]"data-field="price"step="0.01">
|
||||
</td>
|
||||
|
||||
<!-- 🔒 AUTO: TTL AMOUNT = TTL/QTY * PRICE -->
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm bg-light"name="items[${index}][ttl_amount]"data-field="ttl_amount"step="0.001"readonly>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm items-input"name="items[${index}][cbm]"data-field="cbm"step="0.0001">
|
||||
</td>
|
||||
|
||||
<!-- 🔒 AUTO: TTL CBM = CBM * QTY -->
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm bg-light"name="items[${index}][ttl_cbm]"data-field="ttl_cbm"step="0.0001"readonly>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm items-input"name="items[${index}][kg]"data-field="kg"step="0.0001">
|
||||
</td>
|
||||
|
||||
<!-- 🔒 AUTO: TTL KG = CTN * KG -->
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm bg-light"name="items[${index}][ttl_kg]"data-field="ttl_kg"step="0.0001"readonly>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm items-input"name="items[${index}][shop_no]"data-field="shop_no">
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-danger remove-row-btn">×</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
itemsTableBody.appendChild(tr);
|
||||
}
|
||||
|
||||
function calculateRow(row) {
|
||||
const ctn = parseFloat(row.querySelector('[data-field="ctn"]')?.value) || 0;
|
||||
const qty = parseFloat(row.querySelector('[data-field="qty"]')?.value) || 0;
|
||||
const price = parseFloat(row.querySelector('[data-field="price"]')?.value) || 0;
|
||||
const cbm = parseFloat(row.querySelector('[data-field="cbm"]')?.value) || 0;
|
||||
const kg = parseFloat(row.querySelector('[data-field="kg"]')?.value) || 0;
|
||||
|
||||
const ttlQty = ctn * qty;
|
||||
const ttlAmount = ttlQty * price;
|
||||
const ttlCbm = cbm * ctn;
|
||||
const ttlKg = ctn * kg;
|
||||
|
||||
row.querySelector('[data-field="ttl_qty"]').value = ttlQty.toFixed(2);
|
||||
row.querySelector('[data-field="ttl_amount"]').value = ttlAmount.toFixed(2);
|
||||
row.querySelector('[data-field="ttl_cbm"]').value = ttlCbm.toFixed(3);
|
||||
row.querySelector('[data-field="ttl_kg"]').value = ttlKg.toFixed(3);
|
||||
}
|
||||
|
||||
itemsTableBody.addEventListener('input', function (e) {
|
||||
const row = e.target.closest('tr');
|
||||
if (!row) return;
|
||||
|
||||
const calcFields = ['ctn', 'qty', 'price', 'cbm', 'kg'];
|
||||
if (calcFields.includes(e.target.dataset.field)) {
|
||||
calculateRow(row);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
function generateDefaultRows() {
|
||||
itemsTableBody.innerHTML = '';
|
||||
addRow(0);
|
||||
addRow(1);
|
||||
focusFirstInput();
|
||||
}
|
||||
|
||||
function reindexRows() {
|
||||
const rows = itemsTableBody.querySelectorAll('tr');
|
||||
rows.forEach((tr, idx) => {
|
||||
tr.querySelector('td:first-child').textContent = idx + 1;
|
||||
tr.querySelectorAll('input').forEach(input => {
|
||||
const field = input.getAttribute('data-field');
|
||||
input.name = `items[${idx}][${field}]`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function rowHasData(row) {
|
||||
const inputs = row.querySelectorAll('input');
|
||||
return Array.from(inputs).some(inp => inp.value.trim() !== '');
|
||||
}
|
||||
|
||||
function focusFirstInput() {
|
||||
const first = itemsTableBody.querySelector('tr:first-child input');
|
||||
if (first) first.focus();
|
||||
}
|
||||
|
||||
// ------- EXISTING PAGINATION INITIALIZE -------
|
||||
initializePagination();
|
||||
|
||||
// Reset temp data function
|
||||
@@ -1473,7 +1803,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
modal.classList.add('show');
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.querySelector('.alert-success')?.remove();
|
||||
clearForm();
|
||||
generateDefaultRows();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
@@ -1487,11 +1817,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
@endif
|
||||
};
|
||||
|
||||
// Clear form function
|
||||
// Clear form -> clear items table
|
||||
const clearForm = () => {
|
||||
['itemDescription','itemCtn','itemQty','itemTtlQty','itemUnit','itemPrice','itemTtlAmount','itemCbm','itemTtlCbm','itemKg','itemTtlKg','itemShopNo']
|
||||
.forEach(id => document.getElementById(id).value = '');
|
||||
document.getElementById('itemDescription').focus();
|
||||
generateDefaultRows();
|
||||
};
|
||||
|
||||
// Event listeners
|
||||
@@ -1502,7 +1830,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
modal.addEventListener('click', (e) => e.target === modal && closeModal());
|
||||
document.addEventListener('keydown', (e) => e.key === 'Escape' && modal.classList.contains('show') && closeModal());
|
||||
|
||||
// Mark No functionality
|
||||
// Mark No functionality (unchanged)
|
||||
const markNoSelect = document.getElementById('markNoSelect');
|
||||
if (markNoSelect) {
|
||||
markNoSelect.addEventListener('change', function() {
|
||||
@@ -1522,7 +1850,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
@if(session('temp_order_items') && count(session('temp_order_items')) > 0)
|
||||
modal.classList.add('show');
|
||||
document.body.style.overflow = 'hidden';
|
||||
clearForm();
|
||||
generateDefaultRows();
|
||||
@endif
|
||||
|
||||
// Reset confirmation
|
||||
@@ -1533,16 +1861,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
}));
|
||||
|
||||
// Order details modal functionality
|
||||
// Order details modal functionality (unchanged)
|
||||
document.querySelectorAll('.open-order-modal').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
let id = this.dataset.id;
|
||||
let modal = new bootstrap.Modal(document.getElementById('orderDetailsModal'));
|
||||
let modalInstance = new bootstrap.Modal(document.getElementById('orderDetailsModal'));
|
||||
|
||||
document.getElementById('orderDetailsBody').innerHTML =
|
||||
"<p class='text-center text-muted'>Loading...</p>";
|
||||
|
||||
modal.show();
|
||||
modalInstance.show();
|
||||
|
||||
fetch(`/admin/orders/view/${id}`)
|
||||
.then(response => response.text())
|
||||
@@ -1556,12 +1884,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- Pagination Functions ---------- */
|
||||
/* ---------- Pagination Functions (unchanged) ---------- */
|
||||
function initializePagination() {
|
||||
renderOrdersTable(allOrders);
|
||||
updatePaginationControls();
|
||||
|
||||
// Bind pagination buttons
|
||||
document.getElementById('prevPageBtn').addEventListener('click', goToPreviousPage);
|
||||
document.getElementById('nextPageBtn').addEventListener('click', goToNextPage);
|
||||
}
|
||||
@@ -1593,21 +1920,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
prevBtn.disabled = currentPage === 1;
|
||||
nextBtn.disabled = currentPage === totalPages || totalPages === 0;
|
||||
|
||||
// Update page info text
|
||||
const startIndex = (currentPage - 1) * ordersPerPage + 1;
|
||||
const endIndex = Math.min(currentPage * ordersPerPage, allOrders.length);
|
||||
pageInfo.textContent = `Showing ${startIndex} to ${endIndex} of ${allOrders.length} entries`;
|
||||
|
||||
// Generate page numbers
|
||||
paginationPages.innerHTML = '';
|
||||
|
||||
if (totalPages <= 7) {
|
||||
// Show all pages
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
addPageButton(i, paginationPages);
|
||||
}
|
||||
} else {
|
||||
// Show first page, current page range, and last page
|
||||
addPageButton(1, paginationPages);
|
||||
|
||||
if (currentPage > 3) {
|
||||
@@ -1653,7 +1976,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate pagination
|
||||
const startIndex = (currentPage - 1) * ordersPerPage;
|
||||
const endIndex = startIndex + ordersPerPage;
|
||||
const paginatedOrders = orders.slice(startIndex, endIndex);
|
||||
@@ -1698,17 +2020,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
|
||||
// Re-bind order details modal for newly rendered rows
|
||||
const orderLink = tr.querySelector('.open-order-modal');
|
||||
if (orderLink) {
|
||||
orderLink.addEventListener('click', function() {
|
||||
let id = this.dataset.id;
|
||||
let modal = new bootstrap.Modal(document.getElementById('orderDetailsModal'));
|
||||
let modalInstance = new bootstrap.Modal(document.getElementById('orderDetailsModal'));
|
||||
|
||||
document.getElementById('orderDetailsBody').innerHTML =
|
||||
"<p class='text-center text-muted'>Loading...</p>";
|
||||
|
||||
modal.show();
|
||||
modalInstance.show();
|
||||
|
||||
fetch(`/admin/orders/view/${id}`)
|
||||
.then(response => response.text())
|
||||
@@ -1723,19 +2044,134 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Enter key behavior for items/table
|
||||
itemsTableBody.addEventListener('keydown', function(e) {
|
||||
if (e.key !== 'Enter' || e.target.tagName !== 'INPUT') return;
|
||||
e.preventDefault();
|
||||
|
||||
const currentInput = e.target;
|
||||
const currentRow = currentInput.closest('tr');
|
||||
const rows = Array.from(itemsTableBody.querySelectorAll('tr'));
|
||||
const currentRowIndex = rows.indexOf(currentRow);
|
||||
|
||||
const inputs = Array.from(currentRow.querySelectorAll('input'));
|
||||
const currentInputIndex = inputs.indexOf(currentInput);
|
||||
|
||||
const isLastRow = currentRowIndex === rows.length - 1;
|
||||
const hasData = rowHasData(currentRow);
|
||||
|
||||
if (currentInputIndex < inputs.length - 1) {
|
||||
inputs[currentInputIndex + 1].focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLastRow) {
|
||||
const nextRow = rows[currentRowIndex + 1];
|
||||
const firstInputNextRow = nextRow.querySelector('input');
|
||||
if (firstInputNextRow) firstInputNextRow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLastRow && hasData) {
|
||||
const newIndex = rows.length;
|
||||
addRow(newIndex);
|
||||
reindexRows();
|
||||
const newRow = itemsTableBody.querySelector('tr:last-child');
|
||||
const firstInput = newRow.querySelector('input');
|
||||
if (firstInput) firstInput.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Remove row
|
||||
itemsTableBody.addEventListener('click', function(e) {
|
||||
if (!e.target.classList.contains('remove-row-btn')) return;
|
||||
const rows = itemsTableBody.querySelectorAll('tr');
|
||||
if (rows.length <= 1) {
|
||||
alert('At least one row must exist.');
|
||||
return;
|
||||
}
|
||||
e.target.closest('tr').remove();
|
||||
reindexRows();
|
||||
});
|
||||
|
||||
// ===== EXCEL UPLOAD LOGIC =====
|
||||
const uploadExcelBtn = document.getElementById('uploadExcelBtn');
|
||||
const excelInput = document.getElementById('excelInput');
|
||||
|
||||
if (uploadExcelBtn && excelInput) {
|
||||
|
||||
uploadExcelBtn.addEventListener('click', () => excelInput.click());
|
||||
|
||||
excelInput.addEventListener('change', function () {
|
||||
const file = this.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('excel', file);
|
||||
formData.append('_token', '{{ csrf_token() }}');
|
||||
|
||||
fetch('{{ route("admin.orders.upload.excel.preview") }}', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(async res => {
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.success) {
|
||||
alert('Invalid Excel file');
|
||||
return;
|
||||
}
|
||||
populateItemsTable(res.items);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
alert('Excel upload failed');
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function populateItemsTable(items) {
|
||||
itemsTableBody.innerHTML = '';
|
||||
|
||||
items.forEach((item, index) => {
|
||||
addRow(index);
|
||||
const row = itemsTableBody.children[index];
|
||||
|
||||
row.querySelector('[data-field="description"]').value = item.description ?? '';
|
||||
row.querySelector('[data-field="ctn"]').value = item.ctn ?? 0;
|
||||
row.querySelector('[data-field="qty"]').value = item.qty ?? 0;
|
||||
row.querySelector('[data-field="unit"]').value = item.unit ?? '';
|
||||
row.querySelector('[data-field="price"]').value= item.price ?? 0;
|
||||
row.querySelector('[data-field="cbm"]').value = item.cbm ?? 0;
|
||||
row.querySelector('[data-field="kg"]').value = item.kg ?? 0;
|
||||
row.querySelector('[data-field="shop_no"]').value = item.shop_no ?? '';
|
||||
|
||||
// 🔥 ALWAYS RECALCULATE
|
||||
calculateRow(row);
|
||||
});
|
||||
|
||||
reindexRows();
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.addEventListener("hidden.bs.modal", function () {
|
||||
// Remove Bootstrap backdrops
|
||||
document.querySelectorAll(".modal-backdrop").forEach(el => el.remove());
|
||||
|
||||
// Fix page scroll locking
|
||||
document.body.classList.remove("modal-open");
|
||||
document.body.style.overflow = "";
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@endsection
|
||||
@@ -33,6 +33,97 @@ body {
|
||||
100% { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
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
|
||||
-------------------------------------------------- */
|
||||
@@ -183,6 +274,30 @@ body {
|
||||
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
|
||||
-------------------------------------------------- */
|
||||
@@ -225,7 +340,7 @@ body {
|
||||
-------------------------------------------------- */
|
||||
.amount-breakdown-compact {
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-soft);
|
||||
margin-bottom: 1.5rem;
|
||||
@@ -300,6 +415,15 @@ body {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
HEADER ACTION BUTTONS
|
||||
-------------------------------------------------- */
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
RESPONSIVE DESIGN
|
||||
-------------------------------------------------- */
|
||||
@@ -331,6 +455,37 @@ body {
|
||||
.table-compact {
|
||||
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>
|
||||
|
||||
@@ -339,12 +494,23 @@ body {
|
||||
|
||||
<!-- Invoice Preview Section -->
|
||||
<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 class="card-body-compact">
|
||||
</div>
|
||||
<div class="card-body-compact invoice-preview-wrapper">
|
||||
@include('admin.popup_invoice', [
|
||||
'invoice' => $invoice,
|
||||
'shipment' => $shipment,
|
||||
@@ -353,6 +519,89 @@ body {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Amount Breakdown -->
|
||||
<div class="glass-card">
|
||||
<div class="card-header-compact">
|
||||
<h4>
|
||||
<i class="fas fa-calculator me-2"></i>Amount Breakdown
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body-compact">
|
||||
<div class="amount-breakdown-compact">
|
||||
@php
|
||||
$totalPaid = $invoice->installments->sum('amount');
|
||||
$remaining = $invoice->final_amount_with_gst - $totalPaid;
|
||||
|
||||
// Calculate GST/IGST percentage dynamically
|
||||
$taxPercentage = $invoice->tax_type === 'gst'
|
||||
? ($invoice->cgst_percent + $invoice->sgst_percent)
|
||||
: $invoice->igst_percent;
|
||||
@endphp
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label">Total Amount (Before Tax):</span>
|
||||
<span class="breakdown-value">₹{{ number_format($invoice->final_amount, 2) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label">
|
||||
@if($invoice->tax_type === 'gst')
|
||||
GST ({{ number_format($taxPercentage, 2) }}%)
|
||||
@else
|
||||
IGST ({{ number_format($taxPercentage, 2) }}%)
|
||||
@endif:
|
||||
</span>
|
||||
<span class="breakdown-value text-warning">₹{{ number_format($invoice->gst_amount, 2) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row" style="border-top: 2px solid #e2e8f0; padding-top: 0.75rem;">
|
||||
<span class="breakdown-label fw-bold">Total Invoice Amount (Including GST):</span>
|
||||
<span class="breakdown-value fw-bold text-dark">₹{{ number_format($invoice->final_amount_with_gst, 2) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label text-success">Total Paid:</span>
|
||||
<span class="breakdown-value fw-bold text-success" id="paidAmount">₹{{ number_format($totalPaid, 2) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row" style="border-bottom: none;">
|
||||
<span class="breakdown-label text-danger">Remaining:</span>
|
||||
<span class="breakdown-value fw-bold text-danger" id="remainingAmount">₹{{ number_format($remaining, 2) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- NEW: Customer's Total Amount Due Across All Invoices -->
|
||||
<div class="breakdown-row" style="border-top: 2px solid #3b82f6; background: rgba(59, 130, 246, 0.05);">
|
||||
<span class="breakdown-label fw-bold text-primary">
|
||||
<i class="fas fa-users me-1"></i>Total Amount Due (Including Tax):
|
||||
<small class="d-block text-muted" style="font-size: 0.75rem; font-weight: normal;">
|
||||
</small>
|
||||
</span>
|
||||
<span class="breakdown-value fw-bold text-primary" style="font-size: 1.1rem;">
|
||||
₹{{ number_format($customerTotalDue, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Installment Summary -->
|
||||
<div class="summary-grid-compact mt-3">
|
||||
<div class="summary-card-compact total">
|
||||
<div class="summary-value-compact text-success">₹{{ number_format($invoice->final_amount_with_gst, 2) }}</div>
|
||||
<div class="summary-label-compact">Total Amount</div>
|
||||
</div>
|
||||
<div class="summary-card-compact paid">
|
||||
<div class="summary-value-compact text-primary">₹{{ number_format($totalPaid, 2) }}</div>
|
||||
<div class="summary-label-compact">Total Paid</div>
|
||||
</div>
|
||||
<div class="summary-card-compact remaining">
|
||||
<div class="summary-value-compact text-warning">₹{{ number_format($remaining, 2) }}</div>
|
||||
<div class="summary-label-compact">Remaining</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Invoice Form -->
|
||||
<div class="glass-card">
|
||||
<div class="card-header-compact">
|
||||
@@ -462,81 +711,6 @@ body {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$totalPaid = $invoice->installments->sum('amount');
|
||||
$remaining = $invoice->final_amount_with_gst - $totalPaid;
|
||||
@endphp
|
||||
|
||||
<!-- Amount Breakdown -->
|
||||
<div class="amount-breakdown-compact">
|
||||
<h6 class="fw-bold mb-3 text-dark">
|
||||
<i class="fas fa-calculator me-2"></i>Amount Breakdown
|
||||
</h6>
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label">Total Amount (Before Tax):</span>
|
||||
<span class="breakdown-value">₹{{ number_format($invoice->final_amount, 2) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label">Tax Type:</span>
|
||||
<span class="breakdown-value text-primary">
|
||||
@if($invoice->tax_type === 'gst')
|
||||
GST (CGST + SGST)
|
||||
@else
|
||||
IGST
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label">Tax Percentage:</span>
|
||||
<span class="breakdown-value text-primary">
|
||||
@if($invoice->tax_type === 'gst')
|
||||
{{ $invoice->cgst_percent + $invoice->sgst_percent }}%
|
||||
@else
|
||||
{{ $invoice->igst_percent }}%
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label">GST Amount:</span>
|
||||
<span class="breakdown-value text-warning">₹{{ number_format($invoice->gst_amount, 2) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row" style="border-top: 2px solid #e2e8f0; padding-top: 0.75rem;">
|
||||
<span class="breakdown-label fw-bold">Total Invoice Amount (Including GST):</span>
|
||||
<span class="breakdown-value fw-bold text-dark">₹{{ number_format($invoice->final_amount_with_gst, 2) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label text-success">Total Paid:</span>
|
||||
<span class="breakdown-value fw-bold text-success" id="paidAmount">₹{{ number_format($totalPaid, 2) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row" style="border-bottom: none;">
|
||||
<span class="breakdown-label text-danger">Remaining:</span>
|
||||
<span class="breakdown-value fw-bold text-danger" id="remainingAmount">₹{{ number_format($remaining, 2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Installment Summary -->
|
||||
<div class="summary-grid-compact">
|
||||
<div class="summary-card-compact total">
|
||||
<div class="summary-value-compact text-success">₹{{ number_format($invoice->final_amount_with_gst, 2) }}</div>
|
||||
<div class="summary-label-compact">Total Amount</div>
|
||||
</div>
|
||||
<div class="summary-card-compact paid">
|
||||
<div class="summary-value-compact text-primary">₹{{ number_format($totalPaid, 2) }}</div>
|
||||
<div class="summary-label-compact">Total Paid</div>
|
||||
</div>
|
||||
<div class="summary-card-compact remaining">
|
||||
<div class="summary-value-compact text-warning">₹{{ number_format($remaining, 2) }}</div>
|
||||
<div class="summary-label-compact">Remaining</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Installment Management -->
|
||||
<div class="glass-card">
|
||||
<div class="card-header-compact d-flex justify-content-between align-items-center">
|
||||
@@ -621,6 +795,7 @@ body {
|
||||
<h6 class="fw-bold mb-2 text-dark">
|
||||
<i class="fas fa-history me-2"></i>Installment History
|
||||
</h6>
|
||||
@if($invoice->installments->count() > 0)
|
||||
<div class="table-responsive">
|
||||
<table class="table-compact">
|
||||
<thead>
|
||||
@@ -661,24 +836,87 @@ body {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add this just above the table -->
|
||||
<div id="noInstallmentsMsg" class="d-none text-center text-muted fw-bold py-4">
|
||||
@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>
|
||||
<table ...>
|
||||
<tbody id="installmentTable">
|
||||
@foreach($invoice->installments as $i)
|
||||
...
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Share Invoice Functionality
|
||||
const shareInvoiceBtn = document.getElementById('shareInvoiceBtn');
|
||||
if (shareInvoiceBtn) {
|
||||
shareInvoiceBtn.addEventListener('click', function() {
|
||||
const shareUrl = window.location.href;
|
||||
const invoiceNo = "{{ $invoice->invoice_no }}";
|
||||
const message = `Invoice #${invoiceNo} - Total: ₹{{ number_format($invoice->final_amount_with_gst, 2) }}\nView: ${shareUrl}`;
|
||||
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
title: `Invoice #${invoiceNo}`,
|
||||
text: `Invoice #${invoiceNo} - Total: ₹{{ number_format($invoice->final_amount_with_gst, 2) }}`,
|
||||
url: shareUrl
|
||||
}).catch(console.error);
|
||||
} else if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(message)
|
||||
.then(() => {
|
||||
alert('Invoice link copied to clipboard!');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
// Fallback to prompt
|
||||
prompt('Copy this link to share:', shareUrl);
|
||||
});
|
||||
} else {
|
||||
prompt('Copy this link to share:', shareUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Print Invoice Function
|
||||
window.printInvoice = function() {
|
||||
const printWindow = window.open('', '_blank');
|
||||
const invoiceContent = document.querySelector('.invoice-preview-wrapper').innerHTML;
|
||||
const printContent = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Invoice {{ $invoice->invoice_no }}</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.invoice-container { max-width: 800px; margin: 0 auto; }
|
||||
.text-end { text-align: right; }
|
||||
.fw-bold { font-weight: bold; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { padding: 8px; border: 1px solid #ddd; }
|
||||
th { background-color: #f8f9fa; }
|
||||
.total-row { background-color: #f8f9fa; font-weight: bold; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="invoice-container">
|
||||
${invoiceContent}
|
||||
</div>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
window.print();
|
||||
setTimeout(function() {
|
||||
window.close();
|
||||
}, 500);
|
||||
};
|
||||
<\/script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
printWindow.document.write(printContent);
|
||||
printWindow.document.close();
|
||||
};
|
||||
|
||||
// Toggle Installment Form
|
||||
const toggleBtn = document.getElementById("toggleInstallmentForm");
|
||||
const formBox = document.getElementById("installmentForm");
|
||||
@@ -699,6 +937,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
|
||||
if (submitForm) {
|
||||
submitForm.addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -724,9 +963,40 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
|
||||
const table = document.querySelector("#installmentTable");
|
||||
const index = table.rows.length + 1;
|
||||
const noInstallmentsMsg = document.getElementById("noInstallmentsMsg");
|
||||
|
||||
table.insertAdjacentHTML("beforeend", `
|
||||
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>
|
||||
@@ -745,15 +1015,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
</tr>
|
||||
`);
|
||||
|
||||
// Update all displayed values using GST fields!
|
||||
// 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);
|
||||
if (document.getElementById("baseAmount")) document.getElementById("baseAmount").textContent = formatINR(data.baseAmount);
|
||||
if (document.getElementById("gstAmount")) document.getElementById("gstAmount").textContent = formatINR(data.gstAmount);
|
||||
if (document.getElementById("totalInvoiceWithGst")) document.getElementById("totalInvoiceWithGst").textContent = formatINR(data.finalAmountWithGst);
|
||||
if (document.getElementById("invoiceStatus")) document.getElementById("invoiceStatus").textContent = data.isCompleted ? "Paid" : "Pending";
|
||||
|
||||
// Update summary cards if used
|
||||
// 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");
|
||||
@@ -775,6 +1041,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
alert("Something went wrong. Please try again.");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Delete Installment
|
||||
document.addEventListener("click", function (e) {
|
||||
@@ -796,15 +1063,27 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
.then(data => {
|
||||
if (data.status === "success") {
|
||||
row.style.opacity = "0";
|
||||
setTimeout(() => row.remove(), 300);
|
||||
setTimeout(() => {
|
||||
row.remove();
|
||||
|
||||
// Update all displayed values using GST fields!
|
||||
// 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);
|
||||
if (document.getElementById("baseAmount")) document.getElementById("baseAmount").textContent = formatINR(data.baseAmount);
|
||||
if (document.getElementById("gstAmount")) document.getElementById("gstAmount").textContent = formatINR(data.gstAmount);
|
||||
if (document.getElementById("totalInvoiceWithGst")) document.getElementById("totalInvoiceWithGst").textContent = formatINR(data.finalAmountWithGst);
|
||||
if (document.getElementById("invoiceStatus")) document.getElementById("invoiceStatus").textContent = data.remaining === 0 ? "Paid" : "Pending";
|
||||
|
||||
// Update summary cards
|
||||
const paidCard = document.querySelector(".summary-card-compact.paid .summary-value-compact");
|
||||
@@ -812,7 +1091,15 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
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>
|
||||
`;
|
||||
}
|
||||
|
||||
alert(data.message);
|
||||
} else {
|
||||
@@ -825,6 +1112,4 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@endsection
|
||||
@@ -1,7 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<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" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet" />
|
||||
@@ -199,7 +201,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,7 +221,34 @@
|
||||
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>
|
||||
<body>
|
||||
<div class="sidebar">
|
||||
@@ -282,12 +311,12 @@
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Profile Update Requests --}}
|
||||
<!-- {{-- Profile Update Requests --}}
|
||||
@can('request.update_profile')
|
||||
<a href="{{ route('admin.profile.requests') }}">
|
||||
<i class="bi bi-person-lines-fill"></i> Profile Update Requests
|
||||
</a>
|
||||
@endcan
|
||||
@endcan -->
|
||||
|
||||
{{-- Staff (NO PERMISSION REQUIRED) --}}
|
||||
<a href="{{ route('admin.staff.index') }}" class="{{ request()->routeIs('admin.staff.*') ? 'active' : '' }}">
|
||||
@@ -346,6 +375,8 @@
|
||||
</div>
|
||||
|
||||
<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') <!-- ⭐ REQUIRED FOR CHAT TO WORK -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const headerToggle = document.getElementById('headerToggle');
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@extends('admin.layouts.app')
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Orders')
|
||||
@section('page-title', 'Orders')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
@section('content')
|
||||
<style>
|
||||
/* ===== GLOBAL RESPONSIVE STYLES ===== */
|
||||
html, body {
|
||||
overflow-x: hidden !important;
|
||||
@@ -553,6 +553,29 @@
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.date-range {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
box-shadow: var(--shadow-sm);
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.date-label {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filter-bar {
|
||||
flex-direction: column;
|
||||
@@ -563,38 +586,60 @@
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
.filter-select,
|
||||
.date-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.date-range {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
<div class="orders-container">
|
||||
<div class="orders-container">
|
||||
<div class="orders-title">
|
||||
<i class="fas fa-box-open"></i> Orders Management
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats-container">
|
||||
@php
|
||||
$totalOrders = $orders->count();
|
||||
$paidInvoices = $orders->filter(function($order) {
|
||||
$status = $order->invoice?->status ?? 'pending';
|
||||
return strtolower($status) === 'paid';
|
||||
})->count();
|
||||
$pendingInvoices = $orders->filter(function($order) {
|
||||
$status = $order->invoice?->status ?? 'pending';
|
||||
return strtolower($status) === 'pending';
|
||||
})->count();
|
||||
$overdueInvoices = $orders->filter(function($order) {
|
||||
$status = $order->invoice?->status ?? 'pending';
|
||||
return strtolower($status) === 'overdue';
|
||||
})->count();
|
||||
@endphp
|
||||
|
||||
<div class="stat-card total">
|
||||
<div class="stat-value">{{ $orders->count() }}</div>
|
||||
<div class="stat-value">{{ $totalOrders }}</div>
|
||||
<div class="stat-label">Total Orders</div>
|
||||
</div>
|
||||
<div class="stat-card paid">
|
||||
<div class="stat-value">{{ $orders->where('invoice.status', 'paid')->count() }}</div>
|
||||
<div class="stat-value">{{ $paidInvoices }}</div>
|
||||
<div class="stat-label">Paid Invoices</div>
|
||||
</div>
|
||||
<div class="stat-card pending">
|
||||
<div class="stat-value">{{ $orders->where('invoice.status', 'pending')->count() }}</div>
|
||||
<div class="stat-value">{{ $pendingInvoices }}</div>
|
||||
<div class="stat-label">Pending Invoices</div>
|
||||
</div>
|
||||
<div class="stat-card overdue">
|
||||
<div class="stat-value">{{ $orders->where('invoice.status', 'overdue')->count() }}</div>
|
||||
<div class="stat-value">{{ $overdueInvoices }}</div>
|
||||
<div class="stat-label">Overdue Invoices</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -617,12 +662,14 @@
|
||||
<i class="fas fa-search search-icon"></i>
|
||||
<input type="text" class="search-input" placeholder="Search orders..." id="searchInput">
|
||||
</div>
|
||||
|
||||
<select class="filter-select" id="statusFilter">
|
||||
<option value="">All Status</option>
|
||||
<option value="">All Invoice Status</option>
|
||||
<option value="paid">Paid</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="overdue">Overdue</option>
|
||||
</select>
|
||||
|
||||
<select class="filter-select" id="shipmentFilter">
|
||||
<option value="">All Shipments</option>
|
||||
<option value="pending">Pending</option>
|
||||
@@ -630,6 +677,13 @@
|
||||
<option value="dispatched">Dispatched</option>
|
||||
<option value="delivered">Delivered</option>
|
||||
</select>
|
||||
|
||||
<div class="date-range">
|
||||
<span class="date-label">From:</span>
|
||||
<input type="date" class="date-input" id="fromDate">
|
||||
<span class="date-label">To:</span>
|
||||
<input type="date" class="date-input" id="toDate">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(isset($orders) && $orders->count() > 0)
|
||||
@@ -660,20 +714,23 @@
|
||||
$mark = $order->markList ?? null;
|
||||
$invoice = $order->invoice ?? null;
|
||||
$shipment = $order->shipments->first() ?? null;
|
||||
$invoiceStatus = strtolower($invoice->status ?? '');
|
||||
$shipmentStatus = strtolower(str_replace('_', ' ', $shipment->status ?? ''));
|
||||
|
||||
// Normalized status values for consistent CSS classes
|
||||
$invoiceStatus = strtolower($invoice?->status ?? 'pending');
|
||||
$shipmentStatus = strtolower($shipment?->status ?? 'pending');
|
||||
$shipmentStatusForCss = str_replace([' ', '-'], '_', $shipmentStatus);
|
||||
@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>{{ $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_number ?? '-' }}</td>
|
||||
|
||||
<td>
|
||||
{{ $invoice?->invoice_date ? date('d-m-Y', strtotime($invoice->invoice_date)) : '-' }}
|
||||
@@ -688,27 +745,19 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
@if($invoice?->status)
|
||||
<span class="status-badge status-{{ $invoiceStatus }}">
|
||||
{{ ucfirst($invoice->status) }}
|
||||
{{ ucfirst($invoiceStatus) }}
|
||||
</span>
|
||||
@else
|
||||
<span class="status-badge status-pending">Pending</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<td>
|
||||
@if($shipment?->status)
|
||||
<span class="status-badge ship-{{ str_replace(' ', '_', $shipmentStatus) }}">
|
||||
<span class="status-badge ship-{{ $shipmentStatusForCss }}">
|
||||
{{ ucfirst($shipmentStatus) }}
|
||||
</span>
|
||||
@else
|
||||
<span class="status-badge ship-pending">Pending</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<td class="text-center">
|
||||
<a href="{{ route('admin.orders.show', $order->id) }}" title="View Details">
|
||||
<a href="{{ route('admin.orders.see', $order->id) }}" title="View Details">
|
||||
<i class="fas fa-eye action-btn"></i>
|
||||
</a>
|
||||
</td>
|
||||
@@ -744,17 +793,14 @@
|
||||
<p class="text-muted">There are currently no orders in the system.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<script>
|
||||
// Pagination state
|
||||
let currentPage = 1;
|
||||
const itemsPerPage = 10;
|
||||
let allOrders = @json($orders);
|
||||
let filteredOrders = [...allOrders];
|
||||
console.log('ORDERS SAMPLE:', allOrders[0]);
|
||||
|
||||
|
||||
let currentPage = 1;
|
||||
const itemsPerPage = 10;
|
||||
let allOrders = {!! $orders->toJson() !!};
|
||||
let filteredOrders = [...allOrders];
|
||||
|
||||
// Status icon helper functions
|
||||
function getInvoiceStatusIcon(status) {
|
||||
@@ -785,26 +831,96 @@ console.log('ORDERS SAMPLE:', allOrders[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Date validation
|
||||
function isValidDate(dateString) {
|
||||
if (!dateString) return false;
|
||||
const date = new Date(dateString);
|
||||
return date instanceof Date && !isNaN(date);
|
||||
}
|
||||
|
||||
// Date comparison helper - FIXED to properly handle order date
|
||||
function isDateBetween(dateToCheck, fromDate, toDate) {
|
||||
if (!dateToCheck) return true; // If no date to check, don't filter it out
|
||||
|
||||
// Parse the date to check (order date)
|
||||
const checkDate = new Date(dateToCheck);
|
||||
if (!isValidDate(checkDate)) return true;
|
||||
|
||||
// Set time to beginning of day for comparison
|
||||
checkDate.setHours(0, 0, 0, 0);
|
||||
|
||||
// Check from date
|
||||
if (fromDate && isValidDate(fromDate)) {
|
||||
const from = new Date(fromDate);
|
||||
from.setHours(0, 0, 0, 0);
|
||||
if (checkDate < from) return false;
|
||||
}
|
||||
|
||||
// Check to date
|
||||
if (toDate && isValidDate(toDate)) {
|
||||
const to = new Date(toDate);
|
||||
to.setHours(23, 59, 59, 999);
|
||||
if (checkDate > to) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Function to check if an order matches all filters
|
||||
function orderMatchesFilters(order, searchTerm, statusFilter, shipmentFilter, fromDate, toDate) {
|
||||
// Search term matching across multiple fields
|
||||
let matchesSearch = true;
|
||||
if (searchTerm) {
|
||||
const searchFields = [
|
||||
order.order_id?.toString().toLowerCase(),
|
||||
order.shipments?.[0]?.shipment_id?.toString().toLowerCase(),
|
||||
order.mark_list?.customer_id?.toString().toLowerCase(),
|
||||
order.mark_list?.company_name?.toString().toLowerCase(),
|
||||
order.invoice?.invoice_number?.toString().toLowerCase(),
|
||||
order.mark_list?.origin?.toString().toLowerCase(),
|
||||
order.mark_list?.destination?.toString().toLowerCase()
|
||||
];
|
||||
matchesSearch = searchFields.some(field => field && field.includes(searchTerm.toLowerCase()));
|
||||
}
|
||||
|
||||
// Invoice status filter with safe access and normalization
|
||||
const invoiceStatus = (order.invoice?.status || 'pending').toLowerCase();
|
||||
const matchesStatus = !statusFilter || invoiceStatus === statusFilter;
|
||||
|
||||
// Shipment status filter with safe access and normalization
|
||||
const shipmentStatus = (order.shipments?.[0]?.status || 'pending').toLowerCase();
|
||||
const matchesShipment = !shipmentFilter || shipmentStatus === shipmentFilter;
|
||||
|
||||
// Date range filter - using order date (created_at)
|
||||
const orderDate = order.created_at;
|
||||
const matchesDate = isDateBetween(orderDate, fromDate, toDate);
|
||||
|
||||
return matchesSearch && matchesStatus && matchesShipment && matchesDate;
|
||||
}
|
||||
|
||||
// Initialize pagination and filters
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Set today as default "to" date
|
||||
renderTable();
|
||||
updatePaginationControls();
|
||||
|
||||
// Bind pagination events
|
||||
// Pagination events
|
||||
document.getElementById('prevPageBtn').addEventListener('click', goToPreviousPage);
|
||||
document.getElementById('nextPageBtn').addEventListener('click', goToNextPage);
|
||||
|
||||
// Bind filter events
|
||||
document.getElementById('searchInput').addEventListener('input', handleSearch);
|
||||
// Filter events
|
||||
document.getElementById('searchInput').addEventListener('input', handleFilter);
|
||||
document.getElementById('statusFilter').addEventListener('change', handleFilter);
|
||||
document.getElementById('shipmentFilter').addEventListener('change', handleFilter);
|
||||
document.getElementById('fromDate').addEventListener('change', handleFilter);
|
||||
document.getElementById('toDate').addEventListener('change', handleFilter);
|
||||
|
||||
// Bind download events
|
||||
// Download buttons
|
||||
document.getElementById('downloadPdf').addEventListener('click', downloadPdf);
|
||||
document.getElementById('downloadExcel').addEventListener('click', downloadExcel);
|
||||
});
|
||||
|
||||
// Download Functions
|
||||
// Download Functions with ALL filter parameters
|
||||
function downloadPdf() {
|
||||
if (filteredOrders.length === 0) {
|
||||
showNotification('No data available to download', 'warning');
|
||||
@@ -813,25 +929,26 @@ console.log('ORDERS SAMPLE:', allOrders[0]);
|
||||
|
||||
showNotification('Preparing PDF download...', 'info');
|
||||
|
||||
// Get current filters for the download
|
||||
// Get all current filters
|
||||
const searchTerm = document.getElementById('searchInput').value;
|
||||
const statusFilter = document.getElementById('statusFilter').value;
|
||||
const shipmentFilter = document.getElementById('shipmentFilter').value;
|
||||
const fromDate = document.getElementById('fromDate').value;
|
||||
const toDate = document.getElementById('toDate').value;
|
||||
|
||||
// Create download URL with filters
|
||||
// Create download URL with all filters - ALWAYS include all filters
|
||||
let downloadUrl = "{{ route('admin.orders.download.pdf') }}";
|
||||
let params = new URLSearchParams();
|
||||
|
||||
if (searchTerm) params.append('search', searchTerm);
|
||||
if (statusFilter) params.append('status', statusFilter);
|
||||
if (shipmentFilter) params.append('shipment', shipmentFilter);
|
||||
// Always append all parameters, even if empty
|
||||
params.append('search', searchTerm || '');
|
||||
params.append('status', statusFilter || '');
|
||||
params.append('shipment', shipmentFilter || '');
|
||||
params.append('from_date', fromDate || '');
|
||||
params.append('to_date', toDate || '');
|
||||
|
||||
if (params.toString()) {
|
||||
downloadUrl += '?' + params.toString();
|
||||
}
|
||||
|
||||
// Trigger download
|
||||
window.location.href = downloadUrl;
|
||||
// Trigger download with all parameters
|
||||
window.location.href = downloadUrl + '?' + params.toString();
|
||||
}
|
||||
|
||||
function downloadExcel() {
|
||||
@@ -842,120 +959,49 @@ console.log('ORDERS SAMPLE:', allOrders[0]);
|
||||
|
||||
showNotification('Preparing Excel download...', 'info');
|
||||
|
||||
// Get current filters for the download
|
||||
// Get all current filters
|
||||
const searchTerm = document.getElementById('searchInput').value;
|
||||
const statusFilter = document.getElementById('statusFilter').value;
|
||||
const shipmentFilter = document.getElementById('shipmentFilter').value;
|
||||
const fromDate = document.getElementById('fromDate').value;
|
||||
const toDate = document.getElementById('toDate').value;
|
||||
|
||||
// Create download URL with filters
|
||||
// Create download URL with all filters - ALWAYS include all filters
|
||||
let downloadUrl = "{{ route('admin.orders.download.excel') }}";
|
||||
let params = new URLSearchParams();
|
||||
|
||||
if (searchTerm) params.append('search', searchTerm);
|
||||
if (statusFilter) params.append('status', statusFilter);
|
||||
if (shipmentFilter) params.append('shipment', shipmentFilter);
|
||||
// Always append all parameters, even if empty
|
||||
params.append('search', searchTerm || '');
|
||||
params.append('status', statusFilter || '');
|
||||
params.append('shipment', shipmentFilter || '');
|
||||
params.append('from_date', fromDate || '');
|
||||
params.append('to_date', toDate || '');
|
||||
|
||||
if (params.toString()) {
|
||||
downloadUrl += '?' + params.toString();
|
||||
}
|
||||
|
||||
// Trigger download
|
||||
window.location.href = downloadUrl;
|
||||
}
|
||||
|
||||
// Notification function
|
||||
function showNotification(message, type = 'info') {
|
||||
// Remove existing notification
|
||||
const existingNotification = document.querySelector('.download-notification');
|
||||
if (existingNotification) {
|
||||
existingNotification.remove();
|
||||
}
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `download-notification alert alert-${type}`;
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10000;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
animation: slideIn 0.3s ease;
|
||||
`;
|
||||
|
||||
if (type === 'info') {
|
||||
notification.style.background = 'linear-gradient(135deg, #3b82f6, #1d4ed8)';
|
||||
} else if (type === 'warning') {
|
||||
notification.style.background = 'linear-gradient(135deg, #f59e0b, #d97706)';
|
||||
} else if (type === 'success') {
|
||||
notification.style.background = 'linear-gradient(135deg, #10b981, #059669)';
|
||||
}
|
||||
|
||||
notification.textContent = message;
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideOut 0.3s ease';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Add CSS for notifications
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes slideOut {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Search functionality
|
||||
function handleSearch(e) {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
filterOrders();
|
||||
// Trigger download with all parameters
|
||||
window.location.href = downloadUrl + '?' + params.toString();
|
||||
}
|
||||
|
||||
// Filter functionality
|
||||
function handleFilter() {
|
||||
filterOrders();
|
||||
}
|
||||
|
||||
function filterOrders() {
|
||||
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
||||
const searchTerm = document.getElementById('searchInput').value;
|
||||
const statusFilter = document.getElementById('statusFilter').value;
|
||||
const shipmentFilter = document.getElementById('shipmentFilter').value;
|
||||
const fromDate = document.getElementById('fromDate').value;
|
||||
const toDate = document.getElementById('toDate').value;
|
||||
|
||||
filteredOrders = allOrders.filter(order => {
|
||||
const matchesSearch = !searchTerm ||
|
||||
order.order_id?.toLowerCase().includes(searchTerm) ||
|
||||
order.mark_list?.company_name?.toLowerCase().includes(searchTerm) ||
|
||||
order.invoice?.invoice_number?.toLowerCase().includes(searchTerm);
|
||||
|
||||
|
||||
const matchesStatus = !statusFilter ||
|
||||
(order.invoice?.status && order.invoice.status.toLowerCase() === statusFilter);
|
||||
|
||||
const matchesShipment = !shipmentFilter ||
|
||||
(order.shipments?.[0]?.status && order.shipments[0].status.toLowerCase() === shipmentFilter);
|
||||
|
||||
return matchesSearch && matchesStatus && matchesShipment;
|
||||
});
|
||||
// Apply all filters
|
||||
filteredOrders = allOrders.filter(order =>
|
||||
orderMatchesFilters(order, searchTerm, statusFilter, shipmentFilter, fromDate, toDate)
|
||||
);
|
||||
|
||||
currentPage = 1;
|
||||
renderTable();
|
||||
updatePaginationControls();
|
||||
|
||||
// Update download buttons state
|
||||
document.getElementById('downloadPdf').disabled = filteredOrders.length === 0;
|
||||
document.getElementById('downloadExcel').disabled = filteredOrders.length === 0;
|
||||
const hasResults = filteredOrders.length > 0;
|
||||
document.getElementById('downloadPdf').disabled = !hasResults;
|
||||
document.getElementById('downloadExcel').disabled = !hasResults;
|
||||
}
|
||||
|
||||
// Pagination Functions
|
||||
@@ -1056,8 +1102,11 @@ console.log('ORDERS SAMPLE:', allOrders[0]);
|
||||
const mark = order.mark_list || null;
|
||||
const invoice = order.invoice || null;
|
||||
const shipment = order.shipments?.[0] || null;
|
||||
const invoiceStatus = (invoice?.status || '').toLowerCase();
|
||||
const shipmentStatus = (shipment?.status || '').toLowerCase().replace('_', ' ');
|
||||
|
||||
// Normalized status values matching Blade logic
|
||||
const invoiceStatus = (invoice?.status || 'pending').toLowerCase();
|
||||
const shipmentStatus = (shipment?.status || 'pending').toLowerCase();
|
||||
const shipmentStatusForCss = shipmentStatus.replace(/[ -]/g, '_');
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
@@ -1073,19 +1122,13 @@ console.log('ORDERS SAMPLE:', allOrders[0]);
|
||||
<td>${invoice?.final_amount ? '₹' + Number(invoice.final_amount).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2}) : '-'}</td>
|
||||
<td>${invoice?.final_amount_with_gst ? '₹' + Number(invoice.final_amount_with_gst).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2}) : '-'}</td>
|
||||
<td>
|
||||
${invoice?.status
|
||||
? `<span class="status-badge status-${invoiceStatus}">${getInvoiceStatusIcon(invoiceStatus)}${invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}</span>`
|
||||
: '<span class="status-badge status-pending"><i class="fas fa-clock status-icon"></i>Pending</span>'
|
||||
}
|
||||
<span class="status-badge status-${invoiceStatus}">${getInvoiceStatusIcon(invoiceStatus)}${invoiceStatus.charAt(0).toUpperCase() + invoiceStatus.slice(1)}</span>
|
||||
</td>
|
||||
<td>
|
||||
${shipment?.status
|
||||
? `<span class="status-badge ship-${shipmentStatus.replace(' ', '_')}">${getShipmentStatusIcon(shipmentStatus.replace(' ', '_'))}${shipment.status.charAt(0).toUpperCase() + shipment.status.slice(1)}</span>`
|
||||
: '<span class="status-badge ship-pending"><i class="fas fa-clock status-icon"></i>Pending</span>'
|
||||
}
|
||||
<span class="status-badge ship-${shipmentStatusForCss}">${getShipmentStatusIcon(shipmentStatusForCss)}${shipmentStatus.charAt(0).toUpperCase() + shipmentStatus.slice(1)}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="/admin/orders/${order.id}" title="View Details">
|
||||
<a href="/admin/orders/${order.id}/see" title="View Details">
|
||||
<i class="fas fa-eye action-btn"></i>
|
||||
</a>
|
||||
</td>
|
||||
@@ -1093,6 +1136,60 @@ console.log('ORDERS SAMPLE:', allOrders[0]);
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
// Notification function
|
||||
function showNotification(message, type = 'info') {
|
||||
// Remove existing notification
|
||||
const existingNotification = document.querySelector('.download-notification');
|
||||
if (existingNotification) {
|
||||
existingNotification.remove();
|
||||
}
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `download-notification alert alert-${type}`;
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10000;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
animation: slideIn 0.3s ease;
|
||||
`;
|
||||
|
||||
if (type === 'info') {
|
||||
notification.style.background = 'linear-gradient(135deg, #3b82f6, #1d4ed8)';
|
||||
} else if (type === 'warning') {
|
||||
notification.style.background = 'linear-gradient(135deg, #f59e0b, #d97706)';
|
||||
} else if (type === 'success') {
|
||||
notification.style.background = 'linear-gradient(135deg, #10b981, #059669)';
|
||||
}
|
||||
|
||||
notification.textContent = message;
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideOut 0.3s ease';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Add CSS for notifications
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes slideOut {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
@@ -11,17 +11,18 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h3>Orders Report</h3>
|
||||
|
||||
@if(!empty($filters))
|
||||
<h3>Orders Report</h3>
|
||||
|
||||
@if(!empty($filters))
|
||||
<p>
|
||||
@if($filters['search']) Search: <strong>{{ $filters['search'] }}</strong> @endif
|
||||
@if($filters['status']) | Status: <strong>{{ ucfirst($filters['status']) }}</strong> @endif
|
||||
@if($filters['shipment']) | Shipment: <strong>{{ ucfirst($filters['shipment']) }}</strong> @endif
|
||||
@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
|
||||
@endif
|
||||
|
||||
<table>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order ID</th>
|
||||
@@ -42,29 +43,32 @@
|
||||
<tbody>
|
||||
@forelse($orders as $order)
|
||||
@php
|
||||
$mark = $order->markList ?? null;
|
||||
$invoice = $order->invoice ?? null;
|
||||
$shipment = $order->shipments->first() ?? null;
|
||||
$mark = $order->markList;
|
||||
$invoice = $order->invoice;
|
||||
$shipment = $order->shipments->first();
|
||||
@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>{{ $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 ? 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>
|
||||
<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>
|
||||
@empty
|
||||
<tr><td colspan="13" style="text-align:center">No orders found</td></tr>
|
||||
<tr>
|
||||
<td colspan="13" style="text-align:center">No orders found</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -13,35 +13,41 @@
|
||||
<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')
|
||||
@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">
|
||||
|
||||
|
||||
|
||||
{{-- EDIT ORDER --}} -->
|
||||
<!-- @if($order->status === 'pending')
|
||||
<button class="btn btn-edit-order" onclick="document.getElementById('editOrderForm').style.display='block'">
|
||||
{{-- 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>
|
||||
@else
|
||||
<button class="btn btn-edit-order" disabled><i class="fas fa-edit me-2"></i>Edit Order</button>
|
||||
@endif -->
|
||||
@endif
|
||||
|
||||
<!-- {{-- DELETE ORDER --}}
|
||||
@if($order->status === 'pending')
|
||||
{{-- Delete Order --}}
|
||||
@if($status === 'pending')
|
||||
<form action="{{ route('admin.orders.destroy', $order->id) }}"
|
||||
method="POST"
|
||||
onsubmit="return confirm('Delete this entire order?')">
|
||||
@@ -51,9 +57,10 @@
|
||||
<i class="fas fa-trash-alt me-2"></i>Delete Order
|
||||
</button>
|
||||
</form>
|
||||
@endif -->
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
||||
<!-- </div> -->
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -191,11 +198,10 @@
|
||||
<td>{{ $item->shop_no }}</td>
|
||||
|
||||
<td class="d-flex justify-content-center gap-2">
|
||||
|
||||
@if($status === 'pending')
|
||||
{{-- EDIT BUTTON --}}
|
||||
@can('order.edit')
|
||||
<button
|
||||
type="button"
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-edit-item"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#editItemModal{{ $item->id }}">
|
||||
@@ -203,20 +209,22 @@
|
||||
</button>
|
||||
@endcan
|
||||
|
||||
@can('order.delete')
|
||||
{{-- 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 class="btn btn-sm btn-delete-item">
|
||||
<button type="submit" class="btn btn-sm btn-delete-item">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endcan
|
||||
@endif
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
@@ -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 {
|
||||
|
||||
@@ -11,59 +11,67 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
.container {
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* ================= HEADER FIX ================= */
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
overflow: hidden; /* clears floats */
|
||||
border-bottom: 2px solid #E6EBF0;
|
||||
padding-bottom: 13px;
|
||||
}
|
||||
.logo-company {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.logo {
|
||||
}
|
||||
|
||||
/* LEFT SIDE */
|
||||
.logo-company {
|
||||
float: left;
|
||||
width: 65%;
|
||||
}
|
||||
|
||||
/* RIGHT SIDE */
|
||||
.invoice-details {
|
||||
float: right;
|
||||
width: 35%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Logo */
|
||||
.logo {
|
||||
height: 50px;
|
||||
margin-right: 13px;
|
||||
}
|
||||
.company-details {
|
||||
margin-top: 0;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* Company text */
|
||||
.company-details {
|
||||
font-size: 15px;
|
||||
}
|
||||
.company-title {
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.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 {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* Invoice */
|
||||
.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 {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* Paid / Status */
|
||||
.paid-tag {
|
||||
background: #23BF47;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
@@ -72,8 +80,9 @@
|
||||
font-size: 17px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
.paid-tag:before {
|
||||
}
|
||||
|
||||
.paid-tag:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
@@ -82,99 +91,80 @@
|
||||
height: 10px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.paid-date {
|
||||
}
|
||||
|
||||
.paid-date {
|
||||
font-size: 14px;
|
||||
color: #23BF47;
|
||||
margin-top: 2px;
|
||||
}
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.bill-section {
|
||||
/* ================= REST ================= */
|
||||
|
||||
.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 {
|
||||
}
|
||||
|
||||
.bill-title {
|
||||
font-size: 17px;
|
||||
font-weight: bold;
|
||||
color: #23355D;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.bill-details {
|
||||
}
|
||||
|
||||
.bill-details {
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 9px;
|
||||
margin-bottom: 13px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
th {
|
||||
}
|
||||
|
||||
th {
|
||||
background: #F6F7F9;
|
||||
padding: 10px 0;
|
||||
font-size: 15px;
|
||||
color: #6781A6;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
text-align: left;
|
||||
}
|
||||
td {
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 7px 0;
|
||||
color: #222;
|
||||
font-size: 15px;
|
||||
border: none;
|
||||
text-align: left;
|
||||
}
|
||||
tbody tr:not(:last-child) td {
|
||||
color: #222;
|
||||
}
|
||||
|
||||
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 {
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
.gst-row td {
|
||||
color: #23BF47;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.total-row td {
|
||||
font-weight: bold;
|
||||
font-size: 17px;
|
||||
border-top: 2px solid #E6EBF0;
|
||||
}
|
||||
|
||||
</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>
|
||||
<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>{{ number_format($invoice->gst_amount, 0) }}</td>
|
||||
<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 -->
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" 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;
|
||||
@@ -56,7 +57,7 @@
|
||||
}
|
||||
|
||||
.id-container {
|
||||
margin-bottom: 1rem; /* Reduced from 1.5rem */
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.id-box {
|
||||
@@ -67,6 +68,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 {
|
||||
@@ -82,34 +86,29 @@
|
||||
border-left: 4px solid var(--success);
|
||||
}
|
||||
|
||||
.id-box-accent {
|
||||
border-left: 4px solid var(--warning);
|
||||
}
|
||||
|
||||
.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;
|
||||
margin-bottom: 0.5rem; /* Reduced from 0.75rem */
|
||||
font-size: 1rem;
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.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-icon-accent {
|
||||
background: rgba(243, 156, 18, 0.1);
|
||||
color: var(--warning);
|
||||
.id-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.id-label {
|
||||
@@ -126,76 +125,87 @@
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
margin-bottom: 0;
|
||||
word-break: break-word;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.date-container {
|
||||
background: white;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1rem; /* Reduced from 1.25rem */
|
||||
margin-bottom: 1rem; /* Reduced from 1.5rem */
|
||||
border: 1px solid #e9ecef;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
.date-card {
|
||||
text-align: center;
|
||||
padding: 0.75rem;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.date-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 0.5rem; /* Reduced from 0.75rem */
|
||||
background: var(--secondary);
|
||||
/* Enhanced Date Section with Blue-Purple Gradient */
|
||||
.date-badge {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
background: linear-gradient(135deg, #3498db 0%, #9b59b6 100%);
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
min-width: 140px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.2);
|
||||
}
|
||||
|
||||
.date-label {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
.date-badge:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.3);
|
||||
}
|
||||
|
||||
.date-badge .badge-label {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.date-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
padding: 0.5rem;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.date-connector {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.date-connector i {
|
||||
background: var(--light);
|
||||
padding: 10px;
|
||||
.date-badge .badge-label i {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.date-badge .badge-value {
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.date-badge.due-date {
|
||||
background: linear-gradient(135deg, #3498db 0%, #9b59b6 100%);
|
||||
}
|
||||
|
||||
.date-badge.overdue {
|
||||
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.4); }
|
||||
70% { box-shadow: 0 0 0 6px rgba(231, 76, 60, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(231, 76, 60, 0); }
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
color: #dee2e6;
|
||||
font-weight: 300;
|
||||
padding: 0 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.date-separator i {
|
||||
background: white;
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
color: var(--secondary);
|
||||
border: 2px solid #e9ecef;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 1rem; /* Reduced from 1.5rem */
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
@@ -228,27 +238,18 @@
|
||||
background-color: rgba(52, 152, 219, 0.03);
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
background: var(--light);
|
||||
border-left: 4px solid var(--secondary);
|
||||
}
|
||||
|
||||
.summary-header {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
/* Simplified Summary Section */
|
||||
.summary-container {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.amount-row {
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
border-top: 2px solid #dee2e6;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
@@ -268,26 +269,29 @@
|
||||
|
||||
/* COMPACT HEADER STYLES */
|
||||
.compact-header {
|
||||
margin-bottom: 0.75rem; /* Reduced from default */
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.compact-header .invoice-title {
|
||||
margin-bottom: 0.25rem; /* Reduced gap */
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.compact-header .status-badge {
|
||||
margin-top: 0.25rem; /* Reduced gap */
|
||||
/* Date and status row */
|
||||
.date-status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* GST Totals Section Styles - COMPLETELY REMOVED */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.invoice-container {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.date-connector {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
@@ -295,6 +299,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>
|
||||
@@ -306,6 +370,8 @@
|
||||
============================ -->
|
||||
@php
|
||||
$showActions = $showActions ?? true;
|
||||
|
||||
// REMOVED GST CALCULATION LOGIC
|
||||
@endphp
|
||||
|
||||
<div class="compact-header">
|
||||
@@ -318,6 +384,24 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 text-end">
|
||||
<div class="date-status-row">
|
||||
<!-- Invoice Date -->
|
||||
<div class="date-badge">
|
||||
<div class="badge-label">
|
||||
<i class="fas fa-calendar-alt"></i> INVOICE DATE
|
||||
</div>
|
||||
<div class="badge-value">{{ \Carbon\Carbon::parse($invoice->invoice_date)->format('M d, Y') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Due Date -->
|
||||
<div class="date-badge due-date @if($invoice->status == 'overdue') overdue @endif">
|
||||
<div class="badge-label">
|
||||
<i class="fas fa-clock"></i> DUE DATE
|
||||
</div>
|
||||
<div class="badge-value">{{ \Carbon\Carbon::parse($invoice->due_date)->format('M d, Y') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Badge -->
|
||||
<span class="status-badge
|
||||
@if($invoice->status=='paid') bg-success
|
||||
@elseif($invoice->status=='overdue') bg-danger
|
||||
@@ -333,28 +417,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Three ID Boxes in One Row -->
|
||||
<!-- ============================
|
||||
ORDER & SHIPMENT ID BOXES
|
||||
============================ -->
|
||||
<div class="id-container">
|
||||
<div class="row">
|
||||
<!-- Invoice ID Box -->
|
||||
<div class="col-md-4 mb-3">
|
||||
<!-- Order ID Box -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="id-box id-box-primary">
|
||||
<div class="id-icon id-icon-primary">
|
||||
<i class="fas fa-receipt"></i>
|
||||
</div>
|
||||
<div class="id-label">Invoice ID</div>
|
||||
<div class="id-value">{{ $invoice->invoice_number }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order ID Box -->
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="id-box id-box-secondary">
|
||||
<div class="id-icon id-icon-secondary">
|
||||
<i class="fas fa-shopping-cart"></i>
|
||||
</div>
|
||||
<div class="id-label">Order ID</div>
|
||||
<div class="id-content">
|
||||
<div class="id-label">ORDER ID</div>
|
||||
<div class="id-value">
|
||||
@if($invoice->order && $invoice->order->order_id)
|
||||
{{ $invoice->order->order_id }}
|
||||
@@ -366,18 +443,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shipment ID Box -->
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="id-box id-box-accent">
|
||||
<div class="id-icon id-icon-accent">
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="id-box id-box-secondary">
|
||||
<div class="id-icon id-icon-secondary">
|
||||
<i class="fas fa-shipping-fast"></i>
|
||||
</div>
|
||||
<div class="id-label">Shipment ID</div>
|
||||
<div class="id-content">
|
||||
<div class="id-label">SHIPMENT ID</div>
|
||||
<div class="id-value">
|
||||
@php
|
||||
$shipmentId = 'N/A';
|
||||
// Try multiple ways to get shipment ID
|
||||
if($invoice->shipment && $invoice->shipment->shipment_id) {
|
||||
$shipmentId = $invoice->shipment->shipment_id;
|
||||
} elseif($invoice->shipment_id) {
|
||||
@@ -392,38 +470,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================
|
||||
DATES SECTION
|
||||
============================ -->
|
||||
<div class="date-container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-5">
|
||||
<div class="date-card">
|
||||
<div class="date-icon">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
</div>
|
||||
<div class="date-label">INVOICE DATE</div>
|
||||
<div class="date-value">{{ \Carbon\Carbon::parse($invoice->invoice_date)->format('M d, Y') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="date-connector">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="date-card">
|
||||
<div class="date-icon">
|
||||
<i class="fas fa-clock"></i>
|
||||
</div>
|
||||
<div class="date-label">DUE DATE</div>
|
||||
<div class="date-value @if($invoice->status == 'overdue') text-danger @endif">
|
||||
{{ \Carbon\Carbon::parse($invoice->due_date)->format('M d, Y') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================
|
||||
@@ -520,78 +566,22 @@
|
||||
</div>
|
||||
|
||||
<!-- ============================
|
||||
FINAL SUMMARY
|
||||
GST TOTALS SECTION - COMPLETELY REMOVED
|
||||
============================ -->
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-6">
|
||||
<div class="card summary-card">
|
||||
<div class="card-header summary-header">
|
||||
<h6 class="mb-0 fw-bold">
|
||||
<i class="fas fa-calculator me-2"></i> Final Summary
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">Amount:</span>
|
||||
<span class="fw-bold text-dark">₹{{ number_format($invoice->final_amount,2) }}</span>
|
||||
</div>
|
||||
|
||||
@if($invoice->tax_type === 'gst')
|
||||
{{-- CGST --}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">CGST ({{ $invoice->cgst_percent ?? ($invoice->gst_percent/2) }}%):</span>
|
||||
<span class="fw-bold text-danger">₹{{ number_format($invoice->gst_amount/2, 2) }}</span>
|
||||
</div>
|
||||
|
||||
{{-- SGST --}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">SGST ({{ $invoice->sgst_percent ?? ($invoice->gst_percent/2) }}%):</span>
|
||||
<span class="fw-bold text-danger">₹{{ number_format($invoice->gst_amount/2, 2) }}</span>
|
||||
</div>
|
||||
|
||||
@elseif($invoice->tax_type === 'igst')
|
||||
{{-- IGST --}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">IGST ({{ $invoice->igst_percent ?? $invoice->gst_percent }}%):</span>
|
||||
<span class="fw-bold text-danger">₹{{ number_format($invoice->gst_amount, 2) }}</span>
|
||||
</div>
|
||||
@else
|
||||
{{-- Default GST --}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">GST ({{ $invoice->gst_percent }}%):</span>
|
||||
<span class="fw-bold text-danger">₹{{ number_format($invoice->gst_amount, 2) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center pt-1">
|
||||
<span class="fw-bold text-dark">Total Payable:</span>
|
||||
<span class="fw-bold text-success">₹{{ number_format($invoice->final_amount_with_gst,2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================
|
||||
FOOTER — DOWNLOAD & SHARE
|
||||
============================ -->
|
||||
<div class="mt-4 pt-3 border-top text-center">
|
||||
@if($invoice->pdf_path)
|
||||
<a href="{{ asset($invoice->pdf_path) }}" class="btn btn-primary me-2" download>
|
||||
<!-- <div class="mt-4 pt-3 border-top text-center">
|
||||
<a href="{{ route('admin.invoices.download', $invoice->id) }}"
|
||||
class="btn btn-primary me-2">
|
||||
<i class="fas fa-download me-1"></i> Download PDF
|
||||
</a>
|
||||
|
||||
<button class="btn btn-success" onclick="shareInvoice()">
|
||||
<i class="fas fa-share me-1"></i> Share
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Footer Message -->
|
||||
<div class="mt-4 pt-3 border-top text-center text-muted">
|
||||
<p class="mb-1">Thank you for your business!</p>
|
||||
<p class="mb-0">For any inquiries, contact us at support@Kent Logistic</p>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -600,12 +590,12 @@
|
||||
<!-- ============================
|
||||
SHARE SCRIPT
|
||||
============================ -->
|
||||
<script>
|
||||
<!-- <script>
|
||||
function shareInvoice() {
|
||||
const shareData = {
|
||||
title: "Invoice {{ $invoice->invoice_number }}",
|
||||
text: "Sharing invoice {{ $invoice->invoice_number }}",
|
||||
url: "{{ asset($invoice->pdf_path) }}"
|
||||
url: "{{ route('admin.invoices.download', $invoice->id) }}"
|
||||
};
|
||||
|
||||
if (navigator.share) {
|
||||
@@ -615,6 +605,12 @@
|
||||
alert("Link copied! Sharing not supported on this browser.");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
// Add print functionality
|
||||
function printInvoice() {
|
||||
window.print();
|
||||
}
|
||||
</script> -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -5,75 +5,149 @@
|
||||
@section('content')
|
||||
<div class="container-fluid px-0">
|
||||
|
||||
@php
|
||||
@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
|
||||
@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
|
||||
.badge-pending {
|
||||
background: #fff7ed;
|
||||
color: #c2410c;
|
||||
border: 1px solid #fed7aa;
|
||||
}
|
||||
|
||||
.badge-approved {
|
||||
background: #ecfdf5;
|
||||
color: #047857;
|
||||
border: 1px solid #a7f3d0;
|
||||
}
|
||||
|
||||
.badge-rejected {
|
||||
background: #fef2f2;
|
||||
color: #b91c1c;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
/* ===== Action Buttons ===== */
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.actions .btn {
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ===== Detail Grid ===== */
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 14px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* ===== Detail Box ===== */
|
||||
.detail-box {
|
||||
background: #f8fafc;
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.detail-box.changed {
|
||||
background: linear-gradient(145deg, #fff7ed, #ffedd5);
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.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;
|
||||
// FIX: Convert string to array
|
||||
$newData = is_array($req->data) ? $req->data : json_decode($req->data, true);
|
||||
@endphp
|
||||
@endphp
|
||||
|
||||
<tr>
|
||||
<td><strong>{{ ($currentPage - 1) * $perPage + $index + 1 }}</strong></td>
|
||||
<div class="request-card">
|
||||
|
||||
<td>
|
||||
<!-- 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>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
@foreach($newData as $key => $newValue)
|
||||
@php
|
||||
$oldValue = $user->$key ?? '—';
|
||||
$changed = $oldValue != $newValue;
|
||||
@endphp
|
||||
|
||||
<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>
|
||||
|
||||
<td>
|
||||
<div>
|
||||
@if($req->status == 'pending')
|
||||
<span class="badge badge-pending">Pending</span>
|
||||
@elseif($req->status == 'approved')
|
||||
@@ -81,31 +155,57 @@
|
||||
@else
|
||||
<span class="badge badge-rejected">Rejected</span>
|
||||
@endif
|
||||
</td>
|
||||
</div>
|
||||
|
||||
<td>{{ $req->created_at->format('d M Y, h:i A') }}</td>
|
||||
<div>{{ $req->created_at->format('d M Y, h:i A') }}</div>
|
||||
|
||||
<td class="actions">
|
||||
<div 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
|
||||
✔ Approve
|
||||
</a>
|
||||
|
||||
<a href="{{ route('admin.profile.reject', $req->id) }}" class="btn btn-danger btn-sm">
|
||||
<i class="bi bi-x-circle"></i> Reject
|
||||
✖ Reject
|
||||
</a>
|
||||
@else
|
||||
<span class="text-muted">Completed</span>
|
||||
@endif
|
||||
</td>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</tr>
|
||||
<!-- 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
|
||||
</tbody>
|
||||
</table>
|
||||
</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
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@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">
|
||||
|
||||
1388
resources/views/admin/see_order.blade.php
Normal file
1388
resources/views/admin/see_order.blade.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -734,17 +734,31 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Shipment Details Modal */
|
||||
/* Shipment Details Modal - EDGE-TO-EDGE STYLING */
|
||||
.modal-xl.edge-to-edge {
|
||||
max-width: 95vw !important;
|
||||
width: 95vw !important;
|
||||
margin: 1vh auto !important;
|
||||
}
|
||||
|
||||
/* UPDATED: Shipment Order Details Modal - ALSO EDGE-TO-EDGE */
|
||||
.modal-xl.edge-to-edge.order-details-modal {
|
||||
max-width: 95vw !important;
|
||||
width: 95vw !important;
|
||||
margin: 1vh auto !important;
|
||||
}
|
||||
|
||||
.shipment-details-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 25px 30px 15px;
|
||||
padding: 25px 35px 15px;
|
||||
border-radius: 20px 20px 0 0;
|
||||
}
|
||||
|
||||
.shipment-details-body {
|
||||
padding: 40px 45px;
|
||||
padding: 30px 35px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.shipment-info-row {
|
||||
@@ -824,47 +838,63 @@
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
|
||||
/* Shipment Totals Section */
|
||||
/* Shipment Totals Section - SINGLE ROW ON DESKTOP */
|
||||
.shipment-totals {
|
||||
margin-top: 25px;
|
||||
padding: 25px;
|
||||
padding: 25px 20px;
|
||||
background: linear-gradient(135deg, #f8fafc, #e2e8f0);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
||||
border-left: 4px solid #4361ee;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.shipment-totals-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
justify-content: space-between;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.shipment-total-item {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
padding: 15px 10px;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.shipment-total-label {
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.shipment-total-value {
|
||||
font-weight: 800;
|
||||
font-size: 20px;
|
||||
font-size: 18px;
|
||||
color: #1e293b;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.total-amount {
|
||||
@@ -873,6 +903,7 @@
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #10b981;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.total-quantity {
|
||||
@@ -881,6 +912,7 @@
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #0ea5e9;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.total-weight {
|
||||
@@ -889,6 +921,7 @@
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #8b5cf6;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.total-cbm {
|
||||
@@ -897,6 +930,7 @@
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #ef4444;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.total-ctn {
|
||||
@@ -905,6 +939,7 @@
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #f59e0b;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.total-ttl-cbm {
|
||||
@@ -913,6 +948,7 @@
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #8b5cf6;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.total-ttl-kg {
|
||||
@@ -921,6 +957,7 @@
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #14b8a6;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Animation for loading */
|
||||
@@ -1097,6 +1134,48 @@
|
||||
filter: brightness(0) saturate(100%) invert(84%) sepia(8%) saturate(165%) hue-rotate(179deg) brightness(89%) contrast(86%);
|
||||
}
|
||||
|
||||
/* RESPONSIVE DESIGN FOR TABLET/MOBILE */
|
||||
@media (max-width: 1200px) {
|
||||
.modal-xl.edge-to-edge {
|
||||
max-width: 96vw !important;
|
||||
width: 96vw !important;
|
||||
margin: 2vh auto !important;
|
||||
}
|
||||
|
||||
.modal-xl.edge-to-edge.order-details-modal {
|
||||
max-width: 96vw !important;
|
||||
width: 96vw !important;
|
||||
margin: 2vh auto !important;
|
||||
}
|
||||
|
||||
.shipment-totals-row {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.modal-xl.edge-to-edge {
|
||||
max-width: 95vw !important;
|
||||
width: 95vw !important;
|
||||
margin: 2.5vh auto !important;
|
||||
}
|
||||
|
||||
.modal-xl.edge-to-edge.order-details-modal {
|
||||
max-width: 95vw !important;
|
||||
width: 95vw !important;
|
||||
margin: 2.5vh auto !important;
|
||||
}
|
||||
|
||||
.shipment-details-body {
|
||||
padding: 20px 25px;
|
||||
}
|
||||
|
||||
.shipment-totals-row {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pagination-container {
|
||||
flex-direction: column;
|
||||
@@ -1107,6 +1186,116 @@
|
||||
.pagination-controls {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-xl.edge-to-edge {
|
||||
max-width: 100vw !important;
|
||||
width: 100vw !important;
|
||||
margin: 0 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
}
|
||||
|
||||
.modal-xl.edge-to-edge.order-details-modal {
|
||||
max-width: 100vw !important;
|
||||
width: 100vw !important;
|
||||
margin: 0 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
}
|
||||
|
||||
.shipment-details-body {
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.shipment-info-row {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.shipment-info-item {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.shipment-totals {
|
||||
padding: 20px 15px;
|
||||
}
|
||||
|
||||
.shipment-totals-row {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.shipment-total-item {
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
.shipment-total-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.shipment-total-value {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Order details modal specific responsive styles */
|
||||
.order-details-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.order-details-table {
|
||||
min-width: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.shipment-totals-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modal-xl.edge-to-edge .modal-content {
|
||||
border-radius: 0;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-xl.edge-to-edge.order-details-modal .modal-content {
|
||||
border-radius: 0;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.shipment-details-header {
|
||||
padding: 20px 25px 15px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.order-details-header {
|
||||
padding: 20px 15px 10px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.order-details-body {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.modal-xl.edge-to-edge,
|
||||
.modal-xl.edge-to-edge.order-details-modal {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.shipment-details-body,
|
||||
.order-details-body {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1135,11 +1324,16 @@
|
||||
<div class="status-filter-container">
|
||||
<select id="statusFilter" class="status-filter-select">
|
||||
<option value="all">All Status</option>
|
||||
<option value="loading">Loading</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="in_transit">In Transit</option>
|
||||
<option value="dispatched">Dispatched</option>
|
||||
<option value="shipment_ready">Shipment Ready</option>
|
||||
<option value="export_custom">Export Custom</option>
|
||||
<option value="international_transit">International Transit</option>
|
||||
<option value="arrived_india">Arrived at India</option>
|
||||
<option value="import_custom">Import Custom</option>
|
||||
<option value="warehouse">Warehouse</option>
|
||||
<option value="domestic_distribution">Domestic Distribution</option>
|
||||
<option value="out_for_delivery">Out for Delivery</option>
|
||||
<option value="delivered">Delivered</option>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
<select id="carrierFilter">
|
||||
@@ -1206,6 +1400,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@forelse($availableOrders as $order)
|
||||
<tr>
|
||||
<td>
|
||||
@@ -1297,9 +1492,10 @@
|
||||
</td>
|
||||
<td>{{ \Carbon\Carbon::parse($ship->shipment_date)->format('d M Y') }}</td>
|
||||
<td>
|
||||
<button type="button" class="btn-eye" onclick="openShipmentDetails({{ $ship->id }})" title="View Shipment">
|
||||
<a href="{{ route('admin.shipments.dummy', $ship->id) }}"
|
||||
class="btn-view-details">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
@@ -1311,26 +1507,42 @@
|
||||
<form action="{{ route('admin.shipments.updateStatus') }}" method="POST" class="status-form">
|
||||
@csrf
|
||||
<input type="hidden" name="shipment_id" value="{{ $ship->id }}">
|
||||
<button type="submit" name="status" value="loading" class="status-option loading">
|
||||
<span class="status-indicator loading"></span>
|
||||
Loading
|
||||
<button type="submit" name="status" value="shipment_ready" class="status-option shipment_ready">
|
||||
Shipment Ready
|
||||
</button>
|
||||
<button type="submit" name="status" value="pending" class="status-option pending">
|
||||
<span class="status-indicator pending"></span>
|
||||
Pending
|
||||
|
||||
<button type="submit" name="status" value="export_custom" class="status-option export_custom">
|
||||
Export Custom
|
||||
</button>
|
||||
<button type="submit" name="status" value="in_transit" class="status-option in_transit">
|
||||
<span class="status-indicator in_transit"></span>
|
||||
In Transit
|
||||
|
||||
<button type="submit" name="status" value="international_transit" class="status-option international_transit">
|
||||
International Transit
|
||||
</button>
|
||||
<button type="submit" name="status" value="dispatched" class="status-option dispatched">
|
||||
<span class="status-indicator dispatched"></span>
|
||||
Dispatched
|
||||
|
||||
<button type="submit" name="status" value="arrived_india" class="status-option arrived_india">
|
||||
Arrived at India
|
||||
</button>
|
||||
|
||||
<button type="submit" name="status" value="import_custom" class="status-option import_custom">
|
||||
Import Custom
|
||||
</button>
|
||||
|
||||
<button type="submit" name="status" value="warehouse" class="status-option warehouse">
|
||||
Warehouse
|
||||
</button>
|
||||
|
||||
<button type="submit" name="status" value="domestic_distribution" class="status-option domestic_distribution">
|
||||
Domestic Distribution
|
||||
</button>
|
||||
|
||||
<button type="submit" name="status" value="out_for_delivery" class="status-option out_for_delivery">
|
||||
Out for Delivery
|
||||
</button>
|
||||
|
||||
<button type="submit" name="status" value="delivered" class="status-option delivered">
|
||||
<span class="status-indicator delivered"></span>
|
||||
Delivered
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1370,7 +1582,7 @@
|
||||
|
||||
{{-- SHIPMENT DETAILS MODAL --}}
|
||||
<div class="modal fade" id="shipmentDetailsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-dialog modal-xl edge-to-edge modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header shipment-details-header">
|
||||
<h5 class="modal-title fw-bold"><i class="bi bi-box-seam me-2"></i>Consolidated Shipment Details</h5>
|
||||
@@ -1596,7 +1808,7 @@ function renderTable() {
|
||||
<td>${new Date(shipment.shipment_date).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })}</td>
|
||||
|
||||
<td>
|
||||
<a href="{{ route('admin.shipments.dummy', $ship->id) }}"
|
||||
<a href="/admin/shipment/dummy/${shipment.id}"
|
||||
class="btn-view-details">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
@@ -1605,29 +1817,58 @@ function renderTable() {
|
||||
|
||||
<td>
|
||||
<div class="action-container">
|
||||
<button type="button" class="btn-edit-status" onclick="toggleStatusDropdown(this, ${shipment.id})" title="Edit Status">
|
||||
<button type="button"
|
||||
class="btn-edit-status"
|
||||
onclick="toggleStatusDropdown(this, ${shipment.id})"
|
||||
title="Edit Status">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
|
||||
<div class="status-dropdown" id="statusDropdown-${shipment.id}">
|
||||
<form action="/admin/shipments/update-status" method="POST" class="status-form">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="shipment_id" value="${shipment.id}">
|
||||
<button type="submit" name="status" value="loading" class="status-option loading">
|
||||
<span class="status-indicator loading"></span>
|
||||
Loading
|
||||
|
||||
<button type="submit" name="status" value="shipment_ready" class="status-option shipment_ready">
|
||||
<span class="status-indicator shipment_ready"></span>
|
||||
Shipment Ready
|
||||
</button>
|
||||
<button type="submit" name="status" value="pending" class="status-option pending">
|
||||
<span class="status-indicator pending"></span>
|
||||
Pending
|
||||
|
||||
<button type="submit" name="status" value="export_custom" class="status-option export_custom">
|
||||
<span class="status-indicator export_custom"></span>
|
||||
Export Custom
|
||||
</button>
|
||||
<button type="submit" name="status" value="in_transit" class="status-option in_transit">
|
||||
<span class="status-indicator in_transit"></span>
|
||||
In Transit
|
||||
|
||||
<button type="submit" name="status" value="international_transit" class="status-option international_transit">
|
||||
<span class="status-indicator international_transit"></span>
|
||||
International Transit
|
||||
</button>
|
||||
<button type="submit" name="status" value="dispatched" class="status-option dispatched">
|
||||
<span class="status-indicator dispatched"></span>
|
||||
Dispatched
|
||||
|
||||
<button type="submit" name="status" value="arrived_india" class="status-option arrived_india">
|
||||
<span class="status-indicator arrived_india"></span>
|
||||
Arrived at India
|
||||
</button>
|
||||
|
||||
<button type="submit" name="status" value="import_custom" class="status-option import_custom">
|
||||
<span class="status-indicator import_custom"></span>
|
||||
Import Custom
|
||||
</button>
|
||||
|
||||
<button type="submit" name="status" value="warehouse" class="status-option warehouse">
|
||||
<span class="status-indicator warehouse"></span>
|
||||
Warehouse
|
||||
</button>
|
||||
|
||||
<button type="submit" name="status" value="domestic_distribution" class="status-option domestic_distribution">
|
||||
<span class="status-indicator domestic_distribution"></span>
|
||||
Domestic Distribution
|
||||
</button>
|
||||
|
||||
<button type="submit" name="status" value="out_for_delivery" class="status-option out_for_delivery">
|
||||
<span class="status-indicator out_for_delivery"></span>
|
||||
Out for Delivery
|
||||
</button>
|
||||
|
||||
<button type="submit" name="status" value="delivered" class="status-option delivered">
|
||||
<span class="status-indicator delivered"></span>
|
||||
Delivered
|
||||
@@ -1635,7 +1876,8 @@ function renderTable() {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</td>
|
||||
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
@@ -1665,6 +1907,8 @@ function openShipmentDetails(id) {
|
||||
.then(data => {
|
||||
// Format date properly
|
||||
const shipmentDate = new Date(data.shipment.shipment_date);
|
||||
// <div class="shipment-info-value">${data.shipment.shipment_id}</div>
|
||||
|
||||
const formattedDate = shipmentDate.toLocaleDateString('en-GB', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
@@ -1725,7 +1969,11 @@ function openShipmentDetails(id) {
|
||||
html += `
|
||||
<tr>
|
||||
<td class="fw-bold">
|
||||
<a href="javascript:void(0)"
|
||||
class="text-primary fw-bold"
|
||||
onclick="openShipmentOrderDetails(${order.id})">
|
||||
${order.order_id}
|
||||
</a>
|
||||
</td>
|
||||
<td>${order.origin || 'N/A'}</td>
|
||||
<td>${order.destination || 'N/A'}</td>
|
||||
@@ -1838,6 +2086,46 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
function openShipmentOrderDetails(orderId) {
|
||||
const modal = new bootstrap.Modal(
|
||||
document.getElementById('shipmentOrderDetailsModal')
|
||||
);
|
||||
|
||||
const body = document.getElementById('shipmentOrderDetailsBody');
|
||||
body.innerHTML = "<p class='text-center text-muted'>Loading...</p>";
|
||||
|
||||
modal.show();
|
||||
|
||||
fetch(`/admin/orders/view/${orderId}`)
|
||||
.then(res => res.text())
|
||||
.then(html => {
|
||||
body.innerHTML = html;
|
||||
})
|
||||
.catch(() => {
|
||||
body.innerHTML =
|
||||
"<p class='text-danger text-center'>Failed to load order details.</p>";
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<!-- SHIPMENT ORDER DETAILS MODAL - UPDATED TO EDGE-TO-EDGE -->
|
||||
<div class="modal fade" id="shipmentOrderDetailsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl edge-to-edge order-details-modal modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header shipment-details-header order-details-header">
|
||||
<h5 class="modal-title fw-bold"><i class="bi bi-file-text me-2"></i>Order Details</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body shipment-details-body order-details-body" id="shipmentOrderDetailsBody">
|
||||
<p class="text-center text-muted">Loading order details...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,30 +1,581 @@
|
||||
@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>
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
<div class="card">
|
||||
@if(session('success'))
|
||||
<div style="padding:.5rem; background:#e6ffed; border:1px solid #b6f0c6; margin-bottom:1rem;">{{ session('success') }}</div>
|
||||
<div class="alert-success">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<table>
|
||||
{{-- 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>
|
||||
@@ -37,29 +588,309 @@
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<tbody id="staffTableBody">
|
||||
@php
|
||||
$totalStaff = count($staff);
|
||||
@endphp
|
||||
@forelse($staff as $s)
|
||||
<tr>
|
||||
<td>{{ $s->id }}</td>
|
||||
<td class="muted">{{ $s->employee_id }}</td>
|
||||
<td>{{ $s->name }}</td>
|
||||
<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>{{ $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?')">
|
||||
<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 class="btn" type="submit">Delete</button>
|
||||
<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="muted">No staff found.</td></tr>
|
||||
<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>
|
||||
|
||||
<script>
|
||||
// Pagination state
|
||||
let currentPage = 1;
|
||||
const itemsPerPage = 10;
|
||||
let allStaff = @json($staff);
|
||||
let filteredStaff = [...allStaff];
|
||||
|
||||
// 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
@@ -1,12 +1,17 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\RequestController;
|
||||
use App\Http\Controllers\UserAuthController;
|
||||
use App\Http\Controllers\MarkListController;
|
||||
use App\Http\Controllers\User\UserOrderController;
|
||||
use App\Http\Controllers\User\UserProfileController;
|
||||
|
||||
use App\Http\Controllers\User\ChatController;
|
||||
|
||||
//user send request
|
||||
Route::post('/signup-request', [RequestController::class, 'usersignup']);
|
||||
@@ -15,9 +20,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']);
|
||||
|
||||
@@ -32,9 +38,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']);
|
||||
|
||||
@@ -46,4 +54,40 @@ Route::middleware(['auth:api'])->group(function () {
|
||||
Route::post('/user/profile-update-request', [UserProfileController::class, 'updateProfileRequest']);
|
||||
|
||||
// Route::post('/user/profile/update', [UserProfileController::class, 'updateProfile']);
|
||||
|
||||
// ===========================
|
||||
// CHAT SUPPORT ROUTES
|
||||
// ===========================
|
||||
Route::get('/user/chat/start', [ChatController::class, 'startChat']);
|
||||
Route::get('/user/chat/messages/{ticketId}', [ChatController::class, 'getMessages']);
|
||||
Route::post('/user/chat/send/{ticketId}', [ChatController::class, 'sendMessage']);
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
78
routes/channels.php
Normal file
78
routes/channels.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
use App\Models\SupportTicket;
|
||||
use App\Models\Admin;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
file_put_contents(storage_path('logs/broadcast_debug.log'), now()." CHANNELS LOADED\n", FILE_APPEND);
|
||||
|
||||
Broadcast::routes([
|
||||
'middleware' => ['web', 'auth:admin'],
|
||||
]);
|
||||
|
||||
Broadcast::channel('ticket.{ticketId}', function ($user, $ticketId) {
|
||||
|
||||
try {
|
||||
// Very explicit logging to see what arrives here
|
||||
Log::info("CHANNEL AUTH CHECK (ENTER)", [
|
||||
'user_present' => $user !== null,
|
||||
'user_type' => is_object($user) ? get_class($user) : gettype($user),
|
||||
'user_id' => $user->id ?? null,
|
||||
'ticketId' => $ticketId,
|
||||
]);
|
||||
|
||||
// Find ticket and log
|
||||
$ticket = SupportTicket::find($ticketId);
|
||||
Log::info("CHANNEL AUTH: found ticket", [
|
||||
'ticket_exists' => $ticket ? true : false,
|
||||
'ticket_id' => $ticket?->id,
|
||||
'ticket_user_id' => $ticket?->user_id,
|
||||
]);
|
||||
|
||||
if (! $ticket) {
|
||||
Log::warning("CHANNEL AUTH: ticket not found", ['ticketId' => $ticketId]);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If admin, allow
|
||||
if ($user instanceof Admin) {
|
||||
Log::info("CHANNEL AUTH: admin allowed", ['admin_id' => $user->id]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If normal user, check ownership
|
||||
if (is_object($user) && isset($user->id)) {
|
||||
$allowed = $ticket->user_id === $user->id;
|
||||
Log::info("CHANNEL AUTH: user allowed check", [
|
||||
'ticket_user_id' => $ticket->user_id,
|
||||
'current_user_id' => $user->id,
|
||||
'allowed' => $allowed
|
||||
]);
|
||||
return $allowed;
|
||||
}
|
||||
|
||||
Log::warning("CHANNEL AUTH: default deny");
|
||||
return false;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("CHANNEL AUTH ERROR", [
|
||||
'message' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
Broadcast::channel('admin.chat', function ($admin) {
|
||||
return auth('admin')->check();
|
||||
});
|
||||
|
||||
// Broadcast::channel('ticket.{ticketId}', function ($admin, $ticketId) {
|
||||
// \Log::info('CHANNEL AUTH OK', [
|
||||
// 'admin_id' => $admin->id,
|
||||
// 'ticketId' => $ticketId,
|
||||
// ]);
|
||||
|
||||
// return true;
|
||||
// });
|
||||
@@ -11,6 +11,11 @@ use App\Http\Controllers\Admin\AdminCustomerController;
|
||||
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\Session\Middleware\StartSession;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
|
||||
// ---------------------------
|
||||
// Public Front Page
|
||||
@@ -23,18 +28,25 @@ Route::get('/', function () {
|
||||
// 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');
|
||||
|
||||
|
||||
|
||||
|
||||
// ==========================================
|
||||
// PROTECTED ADMIN ROUTES (session protected)
|
||||
// ==========================================
|
||||
Route::prefix('admin')
|
||||
->middleware('auth:admin')
|
||||
->middleware(['web', 'auth:admin'])
|
||||
->group(function () {
|
||||
|
||||
// Dashboard
|
||||
@@ -121,6 +133,14 @@ 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)
|
||||
// ---------------------------
|
||||
@@ -140,6 +160,12 @@ Route::prefix('admin')
|
||||
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)
|
||||
// ---------------------------
|
||||
@@ -171,6 +197,16 @@ Route::prefix('admin')
|
||||
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
|
||||
@@ -193,13 +229,17 @@ 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');
|
||||
|
||||
|
||||
//Add New Invoice
|
||||
// //Add New Invoice
|
||||
Route::get('/admin/invoices/create', [InvoiceController::class, 'create'])->name('admin.invoices.create');
|
||||
|
||||
|
||||
@@ -220,13 +260,26 @@ Route::prefix('admin')
|
||||
|
||||
Route::post('/customers/{id}/status', [AdminCustomerController::class, 'toggleStatus'])
|
||||
->name('admin.customers.status');
|
||||
|
||||
|
||||
// Chat list page
|
||||
Route::get('/chat-support', [AdminChatController::class, 'index'])
|
||||
->name('admin.chat_support');
|
||||
|
||||
// Chat window (open specific user's chat)
|
||||
Route::get('/chat-support/{ticketId}', [AdminChatController::class, 'openChat'])
|
||||
->name('admin.chat.open');
|
||||
|
||||
// Admin sending message
|
||||
Route::post('/chat-support/{ticketId}/send', [AdminChatController::class, 'sendMessage'])
|
||||
->name('admin.chat.send');
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// ADMIN ACCOUNT (AJAX) ROUTES
|
||||
// ==========================================
|
||||
Route::prefix('admin/account')
|
||||
->middleware('auth:admin')
|
||||
->middleware(['web', 'auth:admin'])
|
||||
->name('admin.account.')
|
||||
->group(function () {
|
||||
|
||||
@@ -285,7 +338,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');
|
||||
});
|
||||
|
||||
@@ -293,7 +346,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 () {
|
||||
|
||||
@@ -5,7 +5,12 @@ import tailwindcss from '@tailwindcss/vite';
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
laravel({
|
||||
input: ['resources/css/app.css', 'resources/js/app.js'],
|
||||
input: [
|
||||
"resources/css/app.css",
|
||||
"resources/js/app.js",
|
||||
"resources/js/echo.js",
|
||||
],
|
||||
|
||||
refresh: true,
|
||||
}),
|
||||
tailwindcss(),
|
||||
|
||||
Reference in New Issue
Block a user