Compare commits

..

52 Commits

Author SHA256 Message Date
Utkarsh Khedkar
33571a5fd7 Resolve merge conflicts 2026-02-17 14:44:47 +05:30
Utkarsh Khedkar
94e211f87e Add Container field 2026-02-17 14:32:48 +05:30
Utkarsh Khedkar
8b6d3d5fad Account Changes 2025-12-27 11:15:00 +05:30
Utkarsh Khedkar
c89e5bdf7d changes 2025-12-25 18:11:33 +05:30
Utkarsh Khedkar
10af713fa1 Changes 2025-12-25 11:38:02 +05:30
Utkarsh Khedkar
ebb263cd36 Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-25 10:22:36 +05:30
Utkarsh Khedkar
82d9c10130 Dashboard Changes 2025-12-25 10:22:20 +05:30
divya abdar
cb24cf575b changes in order,popup invoice ,invoice pdf,invoice edit,customer add,customer a 2025-12-25 10:19:20 +05:30
Utkarsh Khedkar
e4c07cb838 shipment Changes 2025-12-24 13:36:50 +05:30
Utkarsh Khedkar
f38a5afdd7 shipment Changes 2025-12-24 13:34:44 +05:30
divya abdar
9423c79c80 changes of dashboard and order , requests 2025-12-24 13:32:47 +05:30
divya abdar
8a958b9c48 dashboard order and request file changes 2025-12-24 10:47:20 +05:30
Abhishek Mali
338425535e account 2025-12-23 22:15:45 +05:30
Abhishek Mali
f7856a6755 account 2025-12-23 21:11:53 +05:30
divya abdar
8f95091673 my chnages in customer and staff 2025-12-23 16:32:47 +05:30
divya abdar
7362ef6bdc staff chnages and customer chnages 2025-12-23 16:26:33 +05:30
Abhishek Mali
e872b83ea3 auto calculate 2025-12-23 14:15:03 +05:30
Utkarsh Khedkar
82882e859e changes 2025-12-23 13:09:33 +05:30
Utkarsh Khedkar
2d28e7c1d5 changes 2025-12-23 13:08:26 +05:30
Abhishek Mali
6ccf2cf84e Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-23 12:23:13 +05:30
Abhishek Mali
4637f0b189 excel import 2025-12-23 12:22:35 +05:30
divya abdar
952dd7eddd staff chnages 2025-12-23 12:22:21 +05:30
Abhishek Mali
451be1a533 status 2025-12-23 10:11:24 +05:30
Abhishek Mali
cd9a786ef4 Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-23 09:51:35 +05:30
divya abdar
a6dd919d3f changes of invoice and shipment 2025-12-23 00:44:29 +05:30
Abhishek Mali
e0a8a5c69c status update 2025-12-23 00:36:15 +05:30
divya abdar
7fa03688aa changes of invoice and shipment 2025-12-23 00:30:18 +05:30
Utkarsh Khedkar
1885d3beef Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-22 22:38:45 +05:30
Utkarsh Khedkar
72a81fa111 Dashboard Changes 2025-12-22 22:38:35 +05:30
divya abdar
044bfe5563 changes of shipment 2025-12-22 21:17:29 +05:30
divya abdar
8ca8f05b93 changes of shipment 2025-12-22 21:15:20 +05:30
Abhishek Mali
2741129740 Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-22 19:22:12 +05:30
Abhishek Mali
1bce2be826 amount update 2025-12-22 19:22:01 +05:30
divya abdar
ea2532efc8 invoice pop up invoice edit file chnages 2025-12-22 17:30:47 +05:30
Utkarsh Khedkar
ccce02f43e Account Changes 2025-12-22 16:49:27 +05:30
Utkarsh Khedkar
cdb6cab57d Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-19 17:28:07 +05:30
Utkarsh Khedkar
3941b06355 changes 2025-12-19 17:27:54 +05:30
divya abdar
d2730e78f6 order, report and dashboard changes 2025-12-19 17:08:53 +05:30
Utkarsh Khedkar
80c6e42e0c Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-19 16:21:00 +05:30
Utkarsh Khedkar
e455c271c4 Chat UI Changes 2025-12-19 16:20:43 +05:30
divya abdar
48f7ab82ff minor changes in order and dashboard, records 2025-12-19 16:15:18 +05:30
Abhishek Mali
c4097ecbde employee update 2025-12-19 16:08:34 +05:30
Utkarsh Khedkar
8a0d122e2c Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-19 11:24:28 +05:30
divya abdar
7ef28e06ae order chnages 2025-12-19 11:18:55 +05:30
divya abdar
752f5ee873 order section changes 2025-12-19 11:12:06 +05:30
Utkarsh Khedkar
84bf42f992 changes 2025-12-19 11:04:16 +05:30
Utkarsh Khedkar
fc9a401a8c changes 2025-12-19 11:00:34 +05:30
Abhishek Mali
3590e8f873 download option in invoide 2025-12-19 10:50:36 +05:30
Abhishek Mali
f6fb304b7a chat support download updated 2025-12-18 12:57:01 +05:30
Abhishek Mali
6b41a447bb chat support update 2025-12-17 19:49:14 +05:30
Abhishek Mali
5dc9fc7db4 chat support updates 2025-12-16 10:19:54 +05:30
Abhishek Mali
1aad6b231e chat support 2025-12-15 11:03:30 +05:30
112 changed files with 24781 additions and 6452 deletions

View File

@@ -20,12 +20,12 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=kent_logistics6
DB_USERNAME=root
DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120

View File

@@ -0,0 +1,87 @@
<?php
namespace App\Events;
use App\Models\ChatMessage;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;
class NewChatMessage implements ShouldBroadcast
{
use SerializesModels;
public $message;
/**
* Create a new event instance.
*/
public function __construct(ChatMessage $message)
{
// Safe data only (no heavy relationships in queue)
$this->message = [
'id' => $message->id,
'ticket_id' => $message->ticket_id,
'sender_id' => $message->sender_id,
'sender_type' => $message->sender_type,
'message' => $message->message,
'file_path' => $message->file_path,
'file_type' => $message->file_type,
'created_at' => $message->created_at->toDateTimeString(),
];
// Load sender separately for broadcastWith()
$this->sender = $message->sender;
}
/**
* The channel the event should broadcast on.
*/
public function broadcastOn()
{
return new PrivateChannel('ticket.' . $this->message->ticket_id);
}
/**
* Data sent to frontend (Blade + Flutter)
*/
public function broadcastWith()
{
\Log::info("DEBUG: NewChatMessage broadcasting on channel ticket.".$this->message->ticket_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,
'file_url' => $this->message->file_path
? asset('storage/' . $this->message->file_path)
: null,
'file_type' => $this->message->file_type,
'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";
}
}

View File

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

View File

@@ -0,0 +1,155 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
<<<<<<< HEAD
use App\Models\SupportTicket;
use App\Models\ChatMessage;
use App\Events\NewChatMessage;
use Illuminate\Http\Request;
=======
use Illuminate\Http\Request;
use App\Models\SupportTicket;
use App\Models\ChatMessage;
use App\Events\NewChatMessage;
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
class AdminChatController extends Controller
{
/**
<<<<<<< HEAD
* Page 1: List all customer chat tickets
*/
public function index()
{
$tickets = SupportTicket::with(['user', 'messages' => function($query) {
$query->latest()->limit(1);
}])
->orderBy('updated_at', 'desc')
->get();
return view('admin.chat_support', compact('tickets'));
}
=======
* Page 1: List all active user chats
*/
public function index()
{
$tickets = SupportTicket::with('user')
->withCount([
'messages as unread_count' => function ($q) {
$q->where('sender_type', \App\Models\User::class)
->where('read_by_admin', false);
}
])
->orderBy('updated_at', 'desc')
->get();
return view('admin.chat_support', compact('tickets'));
}
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
/**
* Page 2: Open chat window for a specific user
*/
public function openChat($ticketId)
<<<<<<< HEAD
{
$ticket = SupportTicket::with('user')->findOrFail($ticketId);
$messages = ChatMessage::where('ticket_id', $ticketId)
->orderBy('created_at', 'asc')
->with('sender')
->get();
return view('admin.chat_window', compact('ticket', 'messages'));
}
/**
* Admin sends a message to the user (FIXED - LIVE CHAT)
=======
{
$ticket = SupportTicket::with('user')->findOrFail($ticketId);
// ✅ MARK USER MESSAGES AS READ FOR ADMIN
ChatMessage::where('ticket_id', $ticketId)
->where('sender_type', \App\Models\User::class)
->where('read_by_admin', false)
->update(['read_by_admin' => true]);
$messages = ChatMessage::where('ticket_id', $ticketId)
->orderBy('created_at', 'asc')
->with('sender')
->get();
return view('admin.chat_window', compact('ticket', 'messages'));
}
/**
* Admin sends a message to the user
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
*/
public function sendMessage(Request $request, $ticketId)
{
$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,
<<<<<<< HEAD
=======
'read_by_admin' => true,
'read_by_user' => false,
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
];
// 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');
<<<<<<< HEAD
\Log::info("DEBUG: ChatController sendMessage called", [
=======
\Log::info("DEBUG: ChatController sendMessage called", [
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
'ticket_id' => $ticketId,
'payload' => $request->all()
]);
<<<<<<< HEAD
// 🔥 LIVE CHAT - Queue bypass (100% working)
broadcast(new NewChatMessage($message))->toOthers();
=======
// Broadcast real-time
broadcast(new NewChatMessage($message));
\Log::info("DEBUG: ChatController sendMessage called 79", [
'ticket_id' => $ticketId,
'payload' => $request->all()
]);
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
return response()->json([
'success' => true,
'message' => $message
]);
}
}

View File

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

View File

@@ -3,12 +3,12 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use Mpdf\Mpdf;
use App\Models\InvoiceInstallment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Mpdf\Mpdf;
class AdminInvoiceController extends Controller
{
@@ -17,7 +17,10 @@ class AdminInvoiceController extends Controller
// -------------------------------------------------------------
public function index()
{
$invoices = Invoice::with(['order.shipments'])->latest()->get();
$invoices = Invoice::with(['items', 'customer', 'container'])
->latest()
->get();
return view('admin.invoice', compact('invoices'));
}
@@ -26,15 +29,10 @@ class AdminInvoiceController extends Controller
// -------------------------------------------------------------
public function popup($id)
{
$invoice = Invoice::with(['items', 'order'])->findOrFail($id);
$invoice = Invoice::with(['items', 'customer', 'container'])->findOrFail($id);
$shipment = null;
// Find actual Shipment record
$shipment = \App\Models\Shipment::whereHas('items', function ($q) use ($invoice) {
$q->where('order_id', $invoice->order_id);
})
->first();
return view('admin.popup_invoice', compact('invoice', 'shipment'));
return view('admin.popup_invoice', compact('invoice', 'shipment'));
}
// -------------------------------------------------------------
@@ -42,111 +40,236 @@ class AdminInvoiceController extends Controller
// -------------------------------------------------------------
public function edit($id)
{
$invoice = Invoice::with(['order.shipments'])->findOrFail($id);
$shipment = $invoice->order?->shipments?->first();
$invoice = Invoice::with(['items', 'customer', 'container'])->findOrFail($id);
$shipment = null;
return view('admin.invoice_edit', compact('invoice', 'shipment'));
// ADD THIS SECTION: Calculate customer's total due across all invoices
$customerTotalDue = Invoice::where('customer_id', $invoice->customer_id)
->where('status', '!=', 'cancelled')
->where('status', '!=', 'void')
->sum('final_amount_with_gst');
// Pass the new variable to the view
return view('admin.invoice_edit', compact('invoice', 'shipment', 'customerTotalDue'));
}
// -------------------------------------------------------------
// UPDATE INVOICE
// UPDATE INVOICE (HEADER LEVEL)
// -------------------------------------------------------------
// -------------------------------------------------------------
// UPDATE INVOICE (HEADER LEVEL)
// -------------------------------------------------------------
public function update(Request $request, $id)
{
Log::info("🟡 Invoice Update Request Received", [
Log::info('🟡 Invoice Update Request Received', [
'invoice_id' => $id,
'request' => $request->all()
'request' => $request->all(),
]);
$invoice = Invoice::findOrFail($id);
// 1) VALIDATION
$data = $request->validate([
'invoice_date' => 'required|date',
'due_date' => 'required|date|after_or_equal:invoice_date',
'final_amount' => 'required|numeric|min:0',
'tax_type' => 'required|in:gst,igst',
'tax_percent' => 'required|numeric|min:0|max:28',
'status' => 'required|in:pending,paid,overdue',
'notes' => 'nullable|string',
'invoice_date' => 'required|date',
'due_date' => 'required|date|after_or_equal:invoice_date',
'final_amount' => 'required|numeric|min:0',
'tax_type' => 'required|in:gst,igst',
'tax_percent' => 'required|numeric|min:0|max:28',
'status' => 'required|in:pending,paid,overdue',
'notes' => 'nullable|string',
]);
Log::info("✅ Validated Invoice Update Data", $data);
$finalAmount = floatval($data['final_amount']);
$taxPercent = floatval($data['tax_percent']);
$taxAmount = 0;
Log::info('✅ Validated Invoice Update Data', $data);
// 2) CALCULATE GST / TOTALS
$finalAmount = (float) $data['final_amount'];
$taxPercent = (float) $data['tax_percent'];
if ($data['tax_type'] === 'gst') {
Log::info("🟢 GST Selected", compact('taxPercent'));
Log::info('🟢 GST Selected', compact('taxPercent'));
$data['cgst_percent'] = $taxPercent / 2;
$data['sgst_percent'] = $taxPercent / 2;
$data['igst_percent'] = 0;
} else {
Log::info("🔵 IGST Selected", compact('taxPercent'));
Log::info('🔵 IGST Selected', compact('taxPercent'));
$data['cgst_percent'] = 0;
$data['sgst_percent'] = 0;
$data['igst_percent'] = $taxPercent;
}
$taxAmount = ($finalAmount * $taxPercent) / 100;
$data['gst_amount'] = $taxAmount;
$data['final_amount_with_gst'] = $finalAmount + $taxAmount;
$data['gst_percent'] = $taxPercent;
Log::info("📌 Final Calculated Invoice Values", [
'invoice_id' => $invoice->id,
'final_amount' => $finalAmount,
'gst_amount' => $data['gst_amount'],
'final_amount_with_gst' => $data['final_amount_with_gst'],
'tax_type' => $data['tax_type'],
'cgst_percent' => $data['cgst_percent'],
'sgst_percent' => $data['sgst_percent'],
'igst_percent' => $data['igst_percent'],
$gstAmount = ($finalAmount * $taxPercent) / 100;
$data['gst_amount'] = $gstAmount;
$data['final_amount_with_gst'] = $finalAmount + $gstAmount;
$data['gst_percent'] = $taxPercent;
Log::info('📌 Final Calculated Invoice Values', [
'invoice_id' => $invoice->id,
'final_amount' => $finalAmount,
'gst_amount' => $data['gst_amount'],
'final_amount_with_gst' => $data['final_amount_with_gst'],
'tax_type' => $data['tax_type'],
'cgst_percent' => $data['cgst_percent'],
'sgst_percent' => $data['sgst_percent'],
'igst_percent' => $data['igst_percent'],
]);
// 3) UPDATE DB
$invoice->update($data);
Log::info("✅ Invoice Updated Successfully", [
'invoice_id' => $invoice->id
Log::info('✅ Invoice Updated Successfully', [
'invoice_id' => $invoice->id,
]);
// regenerate PDF
// 4) LOG ACTUAL DB VALUES
$invoice->refresh();
Log::info('🔍 Invoice AFTER UPDATE (DB values)', [
'invoice_id' => $invoice->id,
'final_amount' => $invoice->final_amount,
'gst_percent' => $invoice->gst_percent,
'gst_amount' => $invoice->gst_amount,
'final_amount_with_gst' => $invoice->final_amount_with_gst,
'tax_type' => $invoice->tax_type,
'cgst_percent' => $invoice->cgst_percent,
'sgst_percent' => $invoice->sgst_percent,
'igst_percent' => $invoice->igst_percent,
]);
// 5) REGENERATE PDF
$this->generateInvoicePDF($invoice);
return redirect()
->route('admin.invoices.index')
->with('success', 'Invoice updated & PDF generated successfully.');
}
// -------------------------------------------------------------
// 🔹 UPDATE INVOICE ITEMS (price + ttl_amount)
// -------------------------------------------------------------
public function updateItems(Request $request, Invoice $invoice)
{
Log::info('🟡 Invoice Items Update Request', [
'invoice_id' => $invoice->id,
'payload' => $request->all(),
]);
$data = $request->validate([
'items' => ['required', 'array'],
'items.*.price' => ['required', 'numeric', 'min:0'],
'items.*.ttl_amount' => ['required', 'numeric', 'min:0'],
]);
$itemsInput = $data['items'];
foreach ($itemsInput as $itemId => $itemData) {
$item = InvoiceItem::where('id', $itemId)
->where('invoice_id', $invoice->id)
->first();
if (!$item) {
Log::warning('Invoice item not found or mismatched invoice', [
'invoice_id' => $invoice->id,
'item_id' => $itemId,
]);
continue;
}
$item->price = $itemData['price'];
$item->ttl_amount = $itemData['ttl_amount'];
$item->save();
}
$newBaseAmount = InvoiceItem::where('invoice_id', $invoice->id)
->sum('ttl_amount');
$taxType = $invoice->tax_type;
$cgstPercent = (float) ($invoice->cgst_percent ?? 0);
$sgstPercent = (float) ($invoice->sgst_percent ?? 0);
$igstPercent = (float) ($invoice->igst_percent ?? 0);
$gstPercent = 0;
if ($taxType === 'gst') {
$gstPercent = $cgstPercent + $sgstPercent;
} elseif ($taxType === 'igst') {
$gstPercent = $igstPercent;
}
$gstAmount = $newBaseAmount * $gstPercent / 100;
$finalWithGst = $newBaseAmount + $gstAmount;
$invoice->final_amount = $newBaseAmount;
$invoice->gst_amount = $gstAmount;
$invoice->final_amount_with_gst = $finalWithGst;
$invoice->gst_percent = $gstPercent;
$invoice->save();
Log::info('✅ Invoice items updated & totals recalculated', [
'invoice_id' => $invoice->id,
'final_amount' => $invoice->final_amount,
'gst_amount' => $invoice->gst_amount,
'final_amount_with_gst' => $invoice->final_amount_with_gst,
'tax_type' => $invoice->tax_type,
'cgst_percent' => $invoice->cgst_percent,
'sgst_percent' => $invoice->sgst_percent,
'igst_percent' => $invoice->igst_percent,
]);
return back()->with('success', 'Invoice items updated successfully.');
}
// -------------------------------------------------------------
// PDF GENERATION USING mPDF
// -------------------------------------------------------------
public function generateInvoicePDF($invoice)
{
$invoice->load(['items', 'order.shipments']);
$shipment = $invoice->order?->shipments?->first();
$invoice->load(['items', 'customer', 'container']);
$shipment = null;
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
$folder = public_path('invoices/');
$folder = public_path('invoices/');
if (!file_exists($folder)) {
mkdir($folder, 0777, true);
}
$filePath = $folder . $fileName;
if (file_exists($filePath)) {
unlink($filePath);
}
$mpdf = new Mpdf(['mode' => 'utf-8', 'format' => 'A4', 'default_font' => 'sans-serif']);
$html = view('admin.pdf.invoice', ['invoice' => $invoice, 'shipment' => $shipment])->render();
$mpdf = new Mpdf([
'mode' => 'utf-8',
'format' => 'A4',
'default_font' => 'sans-serif',
]);
$html = view('admin.pdf.invoice', [
'invoice' => $invoice,
'shipment' => $shipment,
])->render();
$mpdf->WriteHTML($html);
$mpdf->Output($filePath, 'F');
$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)
// INSTALLMENTS (ADD)
// -------------------------------------------------------------
public function storeInstallment(Request $request, $invoice_id)
{
@@ -157,16 +280,14 @@ class AdminInvoiceController extends Controller
'amount' => 'required|numeric|min:1',
]);
$invoice = Invoice::findOrFail($invoice_id);
$invoice = Invoice::findOrFail($invoice_id);
$paidTotal = $invoice->installments()->sum('amount');
// Use GST-inclusive total for all calculations/checks
$remaining = $invoice->final_amount_with_gst - $paidTotal;
if ($request->amount > $remaining) {
return response()->json([
'status' => 'error',
'message' => 'Installment amount exceeds remaining balance.'
'status' => 'error',
'message' => 'Installment amount exceeds remaining balance.',
], 422);
}
@@ -180,48 +301,53 @@ class AdminInvoiceController extends Controller
$newPaid = $paidTotal + $request->amount;
// 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([
'status' => 'success',
'message' => 'Installment added successfully.',
'installment' => $installment,
'totalPaid' => $newPaid,
'gstAmount' => $invoice->gst_amount,
'finalAmountWithGst' => $invoice->final_amount_with_gst,
'baseAmount' => $invoice->final_amount,
'remaining' => max(0, $invoice->final_amount_with_gst - $newPaid),
'isCompleted' => $newPaid >= $invoice->final_amount_with_gst
'status' => 'success',
'message' => 'Installment added successfully.',
'installment' => $installment,
'totalPaid' => $newPaid,
'gstAmount' => $invoice->gst_amount,
'finalAmountWithGst' => $invoice->final_amount_with_gst,
'baseAmount' => $invoice->final_amount,
'remaining' => max(0, $invoice->final_amount_with_gst - $newPaid),
'isCompleted' => $newPaid >= $invoice->final_amount_with_gst,
]);
}
// -------------------------------------------------------------
// INSTALLMENTS (DELETE)
// -------------------------------------------------------------
public function deleteInstallment($id)
{
$installment = InvoiceInstallment::findOrFail($id);
$invoice = $installment->invoice;
$invoice = $installment->invoice;
$installment->delete();
$paidTotal = $invoice->installments()->sum('amount');
$remaining = $invoice->final_amount_with_gst - $paidTotal;
// Update status if not fully paid anymore
if ($remaining > 0 && $invoice->status === "paid") {
if ($remaining > 0 && $invoice->status === 'paid') {
$invoice->update(['status' => 'pending']);
$this->generateInvoicePDF($invoice);
}
return response()->json([
'status' => 'success',
'message' => 'Installment deleted.',
'totalPaid' => $paidTotal,
'gstAmount' => $invoice->gst_amount,
'finalAmountWithGst' => $invoice->final_amount_with_gst,
'baseAmount' => $invoice->final_amount,
'remaining' => $remaining,
'isZero' => $paidTotal == 0
'status' => 'success',
'message' => 'Installment deleted.',
'totalPaid' => $paidTotal,
'gstAmount' => $invoice->gst_amount,
'finalAmountWithGst' => $invoice->final_amount_with_gst,
'baseAmount' => $invoice->final_amount,
'remaining' => $remaining,
'isZero' => $paidTotal == 0,
]);
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,577 @@
<?php
namespace App\Http\Controllers;
use App\Models\Container;
use App\Models\ContainerRow;
use App\Models\MarkList;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use Illuminate\Http\Request;
use Maatwebsite\Excel\Facades\Excel;
class ContainerController extends Controller
{
public function index()
{
$containers = Container::with('rows')->latest()->get();
$containers->each(function ($container) {
$rows = $container->rows;
$totalCtn = 0;
$totalQty = 0;
$totalCbm = 0;
$totalKg = 0;
$ctnKeys = ['CTN', 'CTNS'];
$qtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY', 'QTY', 'PCS', 'PIECES'];
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
$getFirstNumeric = function (array $data, array $possibleKeys) {
$normalizedMap = [];
foreach ($data as $key => $value) {
if ($key === null || $key === '') continue;
$normKey = strtoupper((string)$key);
$normKey = str_replace([' ', '/', '-', '.'], '', $normKey);
$normalizedMap[$normKey] = $value;
}
foreach ($possibleKeys as $search) {
$normSearch = strtoupper($search);
$normSearch = str_replace([' ', '/', '-', '.'], '', $normSearch);
foreach ($normalizedMap as $nKey => $value) {
if (
strpos($nKey, $normSearch) !== false &&
(is_numeric($value) || (is_string($value) && is_numeric(trim($value))))
) {
return (float) trim($value);
}
}
}
return 0;
};
foreach ($rows as $row) {
$data = $row->data ?? [];
$totalCtn += $getFirstNumeric($data, $ctnKeys);
$totalQty += $getFirstNumeric($data, $qtyKeys);
$totalCbm += $getFirstNumeric($data, $cbmKeys);
$totalKg += $getFirstNumeric($data, $kgKeys);
}
$container->summary = [
'total_ctn' => round($totalCtn, 2),
'total_qty' => round($totalQty, 2),
'total_cbm' => round($totalCbm, 3),
'total_kg' => round($totalKg, 2),
];
});
return view('admin.container', compact('containers'));
}
public function create()
{
return view('admin.container_create');
}
private function isValidExcelFormat($rows, $header)
{
if (empty($header) || count($rows) < 2) return false;
$validKeywords = [
'MARK', 'DESCRIPTION', 'DESC', 'CTN', 'CTNS', 'QTY', 'TOTALQTY', 'ITLQTY', 'ITL QTY',
'UNIT', 'CBM', 'TOTAL CBM', 'KG', 'TOTAL KG',
'METAL BUCKLE', 'WATCH MOVEMENT', 'STEEL BOTTLE',
'MEHULPAID', 'ITEM NO', 'ITEM NO.', 'SAHILPAID', 'PINAKIN', 'GST',
'MOON LAMP', 'TRANSPARENT BOTTLE', 'PLASTIC FONDANT',
];
$headerText = implode(' ', array_map('strtoupper', $header));
$requiredHeaders = ['CTN', 'QTY', 'DESCRIPTION', 'DESC'];
$hasValidHeaders = false;
foreach ($requiredHeaders as $key) {
if (stripos($headerText, $key) !== false) {
$hasValidHeaders = true;
break;
}
}
if (!$hasValidHeaders) return false;
$dataPreview = '';
for ($i = 0; $i < min(5, count($rows)); $i++) {
$rowText = implode(' ', array_slice($rows[$i], 0, 10));
$dataPreview .= ' ' . strtoupper((string)$rowText);
}
$validMatches = 0;
foreach ($validKeywords as $keyword) {
if (stripos($headerText . $dataPreview, strtoupper($keyword)) !== false) {
$validMatches++;
}
}
return $validMatches >= 3;
}
private function normalizeKey($value): string
{
$norm = strtoupper((string)$value);
return str_replace([' ', '/', '-', '.'], '', $norm);
}
public function store(Request $request)
{
$request->validate([
'container_name' => 'required|string',
'container_number' => 'required|string|unique:containers,container_number',
'container_date' => 'required|date',
'excel_file' => 'required|file|mimes:xls,xlsx',
]);
$file = $request->file('excel_file');
$sheets = Excel::toArray([], $file);
$rows = $sheets[0] ?? [];
if (count($rows) < 2) {
return back()
->withErrors(['excel_file' => 'Excel file is empty.'])
->withInput();
}
// HEADER DETECTION
$headerRowIndex = null;
$header = [];
foreach ($rows as $i => $row) {
$trimmed = array_map(fn($v) => trim((string)$v), $row);
$nonEmpty = array_filter($trimmed, fn($v) => $v !== '');
if (empty($nonEmpty)) continue;
if (count($nonEmpty) >= 4) {
$headerRowIndex = $i;
$header = $trimmed;
break;
}
}
if ($headerRowIndex === null) {
return back()
->withErrors(['excel_file' => 'Header row not found in Excel.'])
->withInput();
}
if (!$this->isValidExcelFormat($rows, $header)) {
return back()
->withErrors(['excel_file' => 'Only MEHUL / SAHIL / PINAKIN / GST loading list formats allowed.'])
->withInput();
}
// COLUMN INDEXES
$essentialColumns = [
'desc_col' => null,
'ctn_col' => null,
'qty_col' => null,
'totalqty_col' => null,
'unit_col' => null,
'price_col' => null,
'amount_col' => null,
'cbm_col' => null,
'totalcbm_col' => null,
'kg_col' => null,
'totalkg_col' => null,
'itemno_col' => null,
];
foreach ($header as $colIndex => $headingText) {
if (empty($headingText)) continue;
$normalized = $this->normalizeKey($headingText);
if (strpos($normalized, 'DESCRIPTION') !== false || strpos($normalized, 'DESC') !== false) {
$essentialColumns['desc_col'] = $colIndex;
} elseif (strpos($normalized, 'CTN') !== false || strpos($normalized, 'CTNS') !== false) {
$essentialColumns['ctn_col'] = $colIndex;
} elseif (
strpos($normalized, 'ITLQTY') !== false ||
strpos($normalized, 'TOTALQTY') !== false ||
strpos($normalized, 'TTLQTY') !== false
) {
$essentialColumns['totalqty_col'] = $colIndex;
} elseif (strpos($normalized, 'QTY') !== false) {
$essentialColumns['qty_col'] = $colIndex;
} elseif (strpos($normalized, 'UNIT') !== false) {
$essentialColumns['unit_col'] = $colIndex;
} elseif (strpos($normalized, 'PRICE') !== false) {
$essentialColumns['price_col'] = $colIndex;
} elseif (strpos($normalized, 'AMOUNT') !== false) {
$essentialColumns['amount_col'] = $colIndex;
} elseif (strpos($normalized, 'TOTALCBM') !== false || strpos($normalized, 'ITLCBM') !== false) {
$essentialColumns['totalcbm_col'] = $colIndex;
} elseif (strpos($normalized, 'CBM') !== false) {
$essentialColumns['cbm_col'] = $colIndex;
} elseif (strpos($normalized, 'TOTALKG') !== false || strpos($normalized, 'TTKG') !== false) {
$essentialColumns['totalkg_col'] = $colIndex;
} elseif (strpos($normalized, 'KG') !== false) {
$essentialColumns['kg_col'] = $colIndex;
} elseif (
strpos($normalized, 'MARKNO') !== false ||
strpos($normalized, 'MARK') !== false ||
strpos($normalized, 'ITEMNO') !== false ||
strpos($normalized, 'ITEM') !== false
) {
$essentialColumns['itemno_col'] = $colIndex;
}
}
if (is_null($essentialColumns['itemno_col'])) {
return back()
->withErrors(['excel_file' => 'Mark / Item column not found in Excel (expected headers like MARK NO / Mark_No / Item_No).'])
->withInput();
}
// ROWS CLEANING
$dataRows = array_slice($rows, $headerRowIndex + 1);
$cleanedRows = [];
$unmatchedRowsData = [];
foreach ($dataRows as $offset => $row) {
$trimmedRow = array_map(fn($v) => trim((string)$v), $row);
$nonEmptyCells = array_filter($trimmedRow, fn($v) => $v !== '');
if (count($nonEmptyCells) < 2) continue;
$rowText = strtoupper(implode(' ', $trimmedRow));
if (
stripos($rowText, 'TOTAL') !== false ||
stripos($rowText, 'TTL') !== false ||
stripos($rowText, 'GRAND') !== false
) {
continue;
}
$descValue = '';
if ($essentialColumns['desc_col'] !== null) {
$descValue = trim($row[$essentialColumns['desc_col']] ?? '');
}
if ($essentialColumns['desc_col'] !== null && $descValue === '' && count($nonEmptyCells) >= 1) {
continue;
}
$cleanedRows[] = [
'row' => $row,
'offset' => $offset,
];
}
// MARK CHECK: strict - collect ALL marks + unmatched rows
$marksFromExcel = [];
foreach ($cleanedRows as $item) {
$row = $item['row'];
$rawMark = $row[$essentialColumns['itemno_col']] ?? null;
$mark = trim((string)($rawMark ?? ''));
if ($mark !== '') {
$marksFromExcel[] = $mark;
}
}
$marksFromExcel = array_values(array_unique($marksFromExcel));
if (empty($marksFromExcel)) {
return back()
->withErrors(['excel_file' => 'No mark numbers found in Excel file.'])
->withInput();
}
$validMarks = MarkList::whereIn('mark_no', $marksFromExcel)
->where('status', 'active')
->pluck('mark_no')
->toArray();
$unmatchedMarks = array_values(array_diff($marksFromExcel, $validMarks));
if (!empty($unmatchedMarks)) {
foreach ($cleanedRows as $item) {
$row = $item['row'];
$offset = $item['offset'];
$rowMark = trim((string)($row[$essentialColumns['itemno_col']] ?? ''));
if ($rowMark === '' || !in_array($rowMark, $unmatchedMarks)) {
continue;
}
$rowData = [];
foreach ($header as $colIndex => $headingText) {
$value = $row[$colIndex] ?? null;
if (is_string($value)) $value = trim($value);
$rowData[$headingText] = $value;
}
$unmatchedRowsData[] = [
'excel_row' => $headerRowIndex + 1 + $offset,
'mark_no' => $rowMark,
'data' => $rowData,
];
}
return back()
->withErrors(['excel_file' => 'Some mark numbers are not found in Mark List. Container not created.'])
->withInput()
->with('unmatched_rows', $unmatchedRowsData);
}
// STEP 1: Marks → customers mapping + grouping
$markRecords = MarkList::whereIn('mark_no', $marksFromExcel)
->where('status', 'active')
->get();
$markToCustomerId = [];
$markToSnapshot = [];
foreach ($markRecords as $mr) {
$markToCustomerId[$mr->mark_no] = $mr->customer_id;
$markToSnapshot[$mr->mark_no] = [
'customer_name' => $mr->customer_name,
'company_name' => $mr->company_name,
'mobile_no' => $mr->mobile_no,
];
}
$groupedByCustomer = [];
foreach ($cleanedRows as $item) {
$row = $item['row'];
$offset = $item['offset'];
$rawMark = $row[$essentialColumns['itemno_col']] ?? null;
$mark = trim((string)($rawMark ?? ''));
if ($mark === '') {
continue;
}
$customerId = $markToCustomerId[$mark] ?? null;
if (!$customerId) {
continue;
}
if (!isset($groupedByCustomer[$customerId])) {
$groupedByCustomer[$customerId] = [];
}
$groupedByCustomer[$customerId][] = [
'row' => $row,
'offset' => $offset,
'mark' => $mark,
];
}
// STEP 2: Container + ContainerRows save
$container = Container::create([
'container_name' => $request->container_name,
'container_number' => $request->container_number,
'container_date' => $request->container_date,
'status' => 'pending',
]);
$path = $file->store('containers');
$container->update(['excel_file' => $path]);
$savedCount = 0;
foreach ($cleanedRows as $item) {
$row = $item['row'];
$offset = $item['offset'];
$data = [];
foreach ($header as $colIndex => $headingText) {
$value = $row[$colIndex] ?? null;
if (is_string($value)) $value = trim($value);
$data[$headingText] = $value;
}
ContainerRow::create([
'container_id' => $container->id,
'row_index' => $headerRowIndex + 1 + $offset,
'data' => $data,
]);
$savedCount++;
}
// STEP 3: per-customer invoices + invoice items
$invoiceCount = 0;
foreach ($groupedByCustomer as $customerId => $rowsForCustomer) {
if (empty($rowsForCustomer)) {
continue;
}
$firstMark = $rowsForCustomer[0]['mark'];
$snap = $markToSnapshot[$firstMark] ?? null;
$invoice = new Invoice();
$invoice->container_id = $container->id;
// $invoice->customer_id = $customerId;
$invoice->invoice_number = $this->generateInvoiceNumber();
$invoice->invoice_date = now()->toDateString();
$invoice->due_date = null;
if ($snap) {
$invoice->customer_name = $snap['customer_name'] ?? null;
$invoice->company_name = $snap['company_name'] ?? null;
$invoice->customer_mobile = $snap['mobile_no'] ?? null;
}
$invoice->final_amount = 0;
$invoice->gst_percent = 0;
$invoice->gst_amount = 0;
$invoice->final_amount_with_gst = 0;
$invoice->customer_email = null;
$invoice->customer_address = null;
$invoice->pincode = null;
$uniqueMarks = array_unique(array_column($rowsForCustomer, 'mark'));
$invoice->notes = 'Auto-created from Container ' . $container->container_number
. ' for Mark(s): ' . implode(', ', $uniqueMarks);
$invoice->pdf_path = null;
$invoice->status = 'pending';
$invoice->save();
$invoiceCount++;
$totalAmount = 0;
foreach ($rowsForCustomer as $item) {
$row = $item['row'];
$description = $essentialColumns['desc_col'] !== null ? ($row[$essentialColumns['desc_col']] ?? null) : null;
$ctn = $essentialColumns['ctn_col'] !== null ? (int)($row[$essentialColumns['ctn_col']] ?? 0) : 0;
$qty = $essentialColumns['qty_col'] !== null ? (int)($row[$essentialColumns['qty_col']] ?? 0) : 0;
$ttlQty = $essentialColumns['totalqty_col'] !== null ? (int)($row[$essentialColumns['totalqty_col']] ?? 0) : $qty;
$unit = $essentialColumns['unit_col'] !== null ? ($row[$essentialColumns['unit_col']] ?? null) : null;
$price = $essentialColumns['price_col'] !== null ? (float)($row[$essentialColumns['price_col']] ?? 0) : 0;
$ttlAmount = $essentialColumns['amount_col'] !== null ? (float)($row[$essentialColumns['amount_col']] ?? 0) : 0;
$cbm = $essentialColumns['cbm_col'] !== null ? (float)($row[$essentialColumns['cbm_col']] ?? 0) : 0;
$ttlCbm = $essentialColumns['totalcbm_col'] !== null ? (float)($row[$essentialColumns['totalcbm_col']] ?? $cbm) : $cbm;
$kg = $essentialColumns['kg_col'] !== null ? (float)($row[$essentialColumns['kg_col']] ?? 0) : 0;
$ttlKg = $essentialColumns['totalkg_col'] !== null ? (float)($row[$essentialColumns['totalkg_col']] ?? $kg) : $kg;
InvoiceItem::create([
'invoice_id' => $invoice->id,
'description'=> $description,
'ctn' => $ctn,
'qty' => $qty,
'ttl_qty' => $ttlQty,
'unit' => $unit,
'price' => $price,
'ttl_amount' => $ttlAmount,
'cbm' => $cbm,
'ttl_cbm' => $ttlCbm,
'kg' => $kg,
'ttl_kg' => $ttlKg,
'shop_no' => null,
]);
$totalAmount += $ttlAmount;
}
$invoice->final_amount = $totalAmount;
$invoice->gst_percent = 0;
$invoice->gst_amount = 0;
$invoice->final_amount_with_gst = $totalAmount;
$invoice->save();
}
$msg = "Container '{$container->container_number}' created with {$savedCount} rows and {$invoiceCount} customer invoice(s).";
return redirect()->route('containers.index')->with('success', $msg);
}
public function show(Container $container)
{
$container->load('rows');
return view('admin.container_show', compact('container'));
}
public function updateRows(Request $request, Container $container)
{
$rowsInput = $request->input('rows', []);
foreach ($rowsInput as $rowId => $cols) {
$row = ContainerRow::where('container_id', $container->id)
->where('id', $rowId)
->first();
if (!$row) continue;
$data = $row->data ?? [];
foreach ($cols as $colHeader => $value) {
$data[$colHeader] = $value;
}
$row->update([
'data' => $data,
]);
}
return redirect()
->route('containers.show', $container->id)
->with('success', 'Excel rows updated successfully.');
}
public function updateStatus(Request $request, Container $container)
{
$request->validate(['status' => 'required|in:pending,in-progress,completed,cancelled']);
$container->update(['status' => $request->status]);
return redirect()->route('containers.index')->with('success', 'Status updated.');
}
public function destroy(Container $container)
{
$container->delete();
return redirect()->route('containers.index')->with('success', 'Container deleted.');
}
private function generateInvoiceNumber(): string
{
$year = now()->format('Y');
$last = Invoice::whereYear('created_at', $year)
->orderBy('id', 'desc')
->first();
if ($last) {
$parts = explode('-', $last->invoice_number);
$seq = 0;
if (count($parts) === 3) {
$seq = (int) $parts[2];
}
$nextSeq = $seq + 1;
} else {
$nextSeq = 1;
}
return 'INV-' . $year . '-' . str_pad($nextSeq, 6, '0', STR_PAD_LEFT);
}
}

View File

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

View File

@@ -0,0 +1,103 @@
<?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,
<<<<<<< HEAD
=======
'client_id' => $request->client_id, // ✅ ADD
'read_by_admin' => false,
'read_by_user' => true,
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
];
// 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
<<<<<<< HEAD
broadcast(new NewChatMessage($message))->toOthers();
=======
broadcast(new NewChatMessage($message));
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
return response()->json([
'success' => true,
'message' => $message
]);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class ChatMessage extends Model
{
use HasFactory;
protected $fillable = [
<<<<<<< HEAD
'ticket_id',
'sender_id',
'sender_type', // user OR admin
'message',
'file_path',
'file_type',
];
=======
'ticket_id',
'sender_id',
'sender_type',
'message',
'file_path',
'file_type',
'read_by_admin',
'read_by_user',
'client_id',
];
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
/**
* The ticket this message belongs to.
*/
public function ticket()
{
return $this->belongsTo(SupportTicket::class, 'ticket_id');
}
/**
* Polymorphic sender (User or Admin)
*/
public function sender()
{
return $this->morphTo();
}
}

30
app/Models/Container.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Container extends Model
{
protected $fillable = [
'container_name',
'container_number',
'container_date',
'status',
'excel_file',
];
protected $casts = [
'container_date' => 'date',
];
public function rows()
{
return $this->hasMany(ContainerRow::class);
}
public function invoices()
{
return $this->hasMany(Invoice::class);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ContainerRow extends Model
{
protected $fillable = [
'container_id',
'row_index',
'data',
];
protected $casts = [
'data' => 'array',
];
public function container()
{
return $this->belongsTo(Container::class);
}
}

View File

@@ -9,41 +9,30 @@ class Invoice extends Model
{
use HasFactory;
protected $fillable = [
'order_id',
'customer_id',
'mark_no',
'invoice_number',
'invoice_date',
'due_date',
'payment_method',
'reference_no',
'status',
'final_amount', // without tax
'tax_type', // gst / igst
'gst_percent', // only used for gst UI input
'cgst_percent',
'sgst_percent',
'igst_percent',
'gst_amount', // total tax amount
'final_amount_with_gst',
'customer_name',
'company_name',
'customer_email',
'customer_mobile',
'customer_address',
'pincode',
'pdf_path',
'notes',
];
protected $fillable = [
'container_id',
'customer_id',
'mark_no',
'invoice_number',
'invoice_date',
'due_date',
'payment_method',
'reference_no',
'status',
'final_amount',
'gst_percent',
'gst_amount',
'final_amount_with_gst',
'customer_name',
'company_name',
'customer_email',
'customer_mobile',
'customer_address',
'pincode',
'pdf_path',
'notes',
];
/****************************
* Relationships
@@ -54,16 +43,28 @@ class Invoice extends Model
return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC');
}
public function order()
// NEW: invoice आता container वर depend
public function container()
{
return $this->belongsTo(Order::class);
return $this->belongsTo(Container::class);
}
// OLD: order() relation काढले आहे
// public function order()
// {
// return $this->belongsTo(Order::class);
// }
public function customer()
{
return $this->belongsTo(User::class, 'customer_id');
}
public function installments()
{
return $this->hasMany(InvoiceInstallment::class);
}
/****************************
* Helper Functions
****************************/
@@ -72,7 +73,7 @@ class Invoice extends Model
public function calculateTotals()
{
$gst = ($this->final_amount * $this->gst_percent) / 100;
$this->gst_amount = $gst;
$this->gst_amount = $gst;
$this->final_amount_with_gst = $this->final_amount + $gst;
}
@@ -82,15 +83,10 @@ class Invoice extends Model
return $this->status === 'pending' && now()->gt($this->due_date);
}
// जर पुढे container → shipment relation असेल तर हा helper नंतर adjust करू
public function getShipment()
{
return $this->order?->shipments?->first();
// आधी order वरून shipment घेत होत; container flow मध्ये नंतर गरज पडल्यास बदलू
return null;
}
public function installments()
{
return $this->hasMany(InvoiceInstallment::class);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class LoadingListItem extends Model
{
protected $fillable = [
'container_id',
'mark',
'description',
'ctn',
'qty',
'total_qty',
'unit',
'price',
'cbm',
'total_cbm',
'kg',
'total_kg',
];
public function container()
{
return $this->belongsTo(Container::class);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"
@@ -84,7 +85,8 @@
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"platform-check": false
},
"minimum-stability": "stable",
"prefer-stable": true

1626
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,11 +6,6 @@ return [
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application, which will be used when the
| framework needs to place the application's name in a notification or
| other UI elements where an application name needs to be displayed.
|
*/
'name' => env('APP_NAME', 'Laravel'),
@@ -19,11 +14,6 @@ return [
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
@@ -32,11 +22,6 @@ return [
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => (bool) env('APP_DEBUG', false),
@@ -45,11 +30,6 @@ return [
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| the application so that it's available within Artisan commands.
|
*/
'url' => env('APP_URL', 'http://localhost'),
@@ -58,11 +38,6 @@ return [
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. The timezone
| is set to "UTC" by default as it is suitable for most use cases.
|
*/
'timezone' => 'UTC',
@@ -71,11 +46,6 @@ return [
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by Laravel's translation / localization methods. This option can be
| set to any locale for which you plan to have translation strings.
|
*/
'locale' => env('APP_LOCALE', 'en'),
@@ -88,11 +58,6 @@ return [
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is utilized by Laravel's encryption services and should be set
| to a random, 32 character string to ensure that all encrypted values
| are secure. You should do this prior to deploying the application.
|
*/
'cipher' => 'AES-256-CBC',
@@ -109,13 +74,6 @@ return [
|--------------------------------------------------------------------------
| Maintenance Mode Driver
|--------------------------------------------------------------------------
|
| These configuration options determine the driver used to determine and
| manage Laravel's "maintenance mode" status. The "cache" driver will
| allow maintenance mode to be controlled across multiple machines.
|
| Supported drivers: "file", "cache"
|
*/
'maintenance' => [
@@ -123,4 +81,53 @@ return [
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
*/
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Arr' => Illuminate\Support\Arr::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Http' => Illuminate\Support\Facades\Http::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'RateLimiter' => Illuminate\Support\Facades\RateLimiter::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
// ✅ LaravelExcel facade
'Excel' => Maatwebsite\Excel\Facades\Excel::class,
],
];

View File

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

31
config/broadcasting.php Normal file
View 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',
],
],
];

380
config/excel.php Normal file
View File

@@ -0,0 +1,380 @@
<?php
use Maatwebsite\Excel\Excel;
use PhpOffice\PhpSpreadsheet\Reader\Csv;
return [
'exports' => [
/*
|--------------------------------------------------------------------------
| Chunk size
|--------------------------------------------------------------------------
|
| When using FromQuery, the query is automatically chunked.
| Here you can specify how big the chunk should be.
|
*/
'chunk_size' => 1000,
/*
|--------------------------------------------------------------------------
| Pre-calculate formulas during export
|--------------------------------------------------------------------------
*/
'pre_calculate_formulas' => false,
/*
|--------------------------------------------------------------------------
| Enable strict null comparison
|--------------------------------------------------------------------------
|
| When enabling strict null comparison empty cells ('') will
| be added to the sheet.
*/
'strict_null_comparison' => false,
/*
|--------------------------------------------------------------------------
| CSV Settings
|--------------------------------------------------------------------------
|
| Configure e.g. delimiter, enclosure and line ending for CSV exports.
|
*/
'csv' => [
'delimiter' => ',',
'enclosure' => '"',
'line_ending' => PHP_EOL,
'use_bom' => false,
'include_separator_line' => false,
'excel_compatibility' => false,
'output_encoding' => '',
'test_auto_detect' => true,
],
/*
|--------------------------------------------------------------------------
| Worksheet properties
|--------------------------------------------------------------------------
|
| Configure e.g. default title, creator, subject,...
|
*/
'properties' => [
'creator' => '',
'lastModifiedBy' => '',
'title' => '',
'description' => '',
'subject' => '',
'keywords' => '',
'category' => '',
'manager' => '',
'company' => '',
],
],
'imports' => [
/*
|--------------------------------------------------------------------------
| Read Only
|--------------------------------------------------------------------------
|
| When dealing with imports, you might only be interested in the
| data that the sheet exists. By default we ignore all styles,
| however if you want to do some logic based on style data
| you can enable it by setting read_only to false.
|
*/
'read_only' => true,
/*
|--------------------------------------------------------------------------
| Ignore Empty
|--------------------------------------------------------------------------
|
| When dealing with imports, you might be interested in ignoring
| rows that have null values or empty strings. By default rows
| containing empty strings or empty values are not ignored but can be
| ignored by enabling the setting ignore_empty to true.
|
*/
'ignore_empty' => false,
/*
|--------------------------------------------------------------------------
| Heading Row Formatter
|--------------------------------------------------------------------------
|
| Configure the heading row formatter.
| Available options: none|slug|custom
|
*/
'heading_row' => [
'formatter' => 'slug',
],
/*
|--------------------------------------------------------------------------
| CSV Settings
|--------------------------------------------------------------------------
|
| Configure e.g. delimiter, enclosure and line ending for CSV imports.
|
*/
'csv' => [
'delimiter' => null,
'enclosure' => '"',
'escape_character' => '\\',
'contiguous' => false,
'input_encoding' => Csv::GUESS_ENCODING,
],
/*
|--------------------------------------------------------------------------
| Worksheet properties
|--------------------------------------------------------------------------
|
| Configure e.g. default title, creator, subject,...
|
*/
'properties' => [
'creator' => '',
'lastModifiedBy' => '',
'title' => '',
'description' => '',
'subject' => '',
'keywords' => '',
'category' => '',
'manager' => '',
'company' => '',
],
/*
|--------------------------------------------------------------------------
| Cell Middleware
|--------------------------------------------------------------------------
|
| Configure middleware that is executed on getting a cell value
|
*/
'cells' => [
'middleware' => [
//\Maatwebsite\Excel\Middleware\TrimCellValue::class,
//\Maatwebsite\Excel\Middleware\ConvertEmptyCellValuesToNull::class,
],
],
],
/*
|--------------------------------------------------------------------------
| Extension detector
|--------------------------------------------------------------------------
|
| Configure here which writer/reader type should be used when the package
| needs to guess the correct type based on the extension alone.
|
*/
'extension_detector' => [
'xlsx' => Excel::XLSX,
'xlsm' => Excel::XLSX,
'xltx' => Excel::XLSX,
'xltm' => Excel::XLSX,
'xls' => Excel::XLS,
'xlt' => Excel::XLS,
'ods' => Excel::ODS,
'ots' => Excel::ODS,
'slk' => Excel::SLK,
'xml' => Excel::XML,
'gnumeric' => Excel::GNUMERIC,
'htm' => Excel::HTML,
'html' => Excel::HTML,
'csv' => Excel::CSV,
'tsv' => Excel::TSV,
/*
|--------------------------------------------------------------------------
| PDF Extension
|--------------------------------------------------------------------------
|
| Configure here which Pdf driver should be used by default.
| Available options: Excel::MPDF | Excel::TCPDF | Excel::DOMPDF
|
*/
'pdf' => Excel::DOMPDF,
],
/*
|--------------------------------------------------------------------------
| Value Binder
|--------------------------------------------------------------------------
|
| PhpSpreadsheet offers a way to hook into the process of a value being
| written to a cell. In there some assumptions are made on how the
| value should be formatted. If you want to change those defaults,
| you can implement your own default value binder.
|
| Possible value binders:
|
| [x] Maatwebsite\Excel\DefaultValueBinder::class
| [x] PhpOffice\PhpSpreadsheet\Cell\StringValueBinder::class
| [x] PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder::class
|
*/
'value_binder' => [
'default' => Maatwebsite\Excel\DefaultValueBinder::class,
],
'cache' => [
/*
|--------------------------------------------------------------------------
| Default cell caching driver
|--------------------------------------------------------------------------
|
| By default PhpSpreadsheet keeps all cell values in memory, however when
| dealing with large files, this might result into memory issues. If you
| want to mitigate that, you can configure a cell caching driver here.
| When using the illuminate driver, it will store each value in the
| cache store. This can slow down the process, because it needs to
| store each value. You can use the "batch" store if you want to
| only persist to the store when the memory limit is reached.
|
| Drivers: memory|illuminate|batch
|
*/
'driver' => 'memory',
/*
|--------------------------------------------------------------------------
| Batch memory caching
|--------------------------------------------------------------------------
|
| When dealing with the "batch" caching driver, it will only
| persist to the store when the memory limit is reached.
| Here you can tweak the memory limit to your liking.
|
*/
'batch' => [
'memory_limit' => 60000,
],
/*
|--------------------------------------------------------------------------
| Illuminate cache
|--------------------------------------------------------------------------
|
| When using the "illuminate" caching driver, it will automatically use
| your default cache store. However if you prefer to have the cell
| cache on a separate store, you can configure the store name here.
| You can use any store defined in your cache config. When leaving
| at "null" it will use the default store.
|
*/
'illuminate' => [
'store' => null,
],
/*
|--------------------------------------------------------------------------
| Cache Time-to-live (TTL)
|--------------------------------------------------------------------------
|
| The TTL of items written to cache. If you want to keep the items cached
| indefinitely, set this to null. Otherwise, set a number of seconds,
| a \DateInterval, or a callable.
|
| Allowable types: callable|\DateInterval|int|null
|
*/
'default_ttl' => 10800,
],
/*
|--------------------------------------------------------------------------
| Transaction Handler
|--------------------------------------------------------------------------
|
| By default the import is wrapped in a transaction. This is useful
| for when an import may fail and you want to retry it. With the
| transactions, the previous import gets rolled-back.
|
| You can disable the transaction handler by setting this to null.
| Or you can choose a custom made transaction handler here.
|
| Supported handlers: null|db
|
*/
'transactions' => [
'handler' => 'db',
'db' => [
'connection' => null,
],
],
'temporary_files' => [
/*
|--------------------------------------------------------------------------
| Local Temporary Path
|--------------------------------------------------------------------------
|
| When exporting and importing files, we use a temporary file, before
| storing reading or downloading. Here you can customize that path.
| permissions is an array with the permission flags for the directory (dir)
| and the create file (file).
|
*/
'local_path' => storage_path('framework/cache/laravel-excel'),
/*
|--------------------------------------------------------------------------
| Local Temporary Path Permissions
|--------------------------------------------------------------------------
|
| Permissions is an array with the permission flags for the directory (dir)
| and the create file (file).
| If omitted the default permissions of the filesystem will be used.
|
*/
'local_permissions' => [
// 'dir' => 0755,
// 'file' => 0644,
],
/*
|--------------------------------------------------------------------------
| Remote Temporary Disk
|--------------------------------------------------------------------------
|
| When dealing with a multi server setup with queues in which you
| cannot rely on having a shared local temporary path, you might
| want to store the temporary file on a shared disk. During the
| queue executing, we'll retrieve the temporary file from that
| location instead. When left to null, it will always use
| the local path. This setting only has effect when using
| in conjunction with queued imports and exports.
|
*/
'remote_disk' => null,
'remote_prefix' => null,
/*
|--------------------------------------------------------------------------
| Force Resync
|--------------------------------------------------------------------------
|
| When dealing with a multi server setup as above, it's possible
| for the clean up that occurs after entire queue has been run to only
| cleanup the server that the last AfterImportJob runs on. The rest of the server
| would still have the local temporary file stored on it. In this case your
| local storage limits can be exceeded and future imports won't be processed.
| To mitigate this you can set this config value to be true, so that after every
| queued chunk is processed the local temporary file is deleted on the server that
| processed it.
|
*/
'force_resync_remote' => null,
],
];

View File

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

96
config/reverb.php Normal file
View 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),
],
],
],
];

View File

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

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('chat_messages', function (Blueprint $table) {
$table->boolean('read_by_admin')->default(false)->after('file_type');
$table->boolean('read_by_user')->default(false)->after('read_by_admin');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('chat_messages', function (Blueprint $table) {
//
});
}
};

View File

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

View File

@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('containers', function (Blueprint $table) {
$table->id();
$table->string('container_name');
$table->string('container_number')->unique();
$table->date('container_date');
$table->string('excel_file')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('containers');
}
};

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('loading_list_items', function (Blueprint $table) {
$table->id();
$table->foreignId('container_id')
->constrained('containers')
->onDelete('cascade');
$table->string('mark')->nullable(); // MARK / ITEM NO
$table->string('description')->nullable();
$table->integer('ctn')->nullable();
$table->integer('qty')->nullable();
$table->integer('total_qty')->nullable();
$table->string('unit')->nullable();
$table->decimal('price', 15, 3)->nullable(); // SAHIL format साठी
$table->decimal('cbm', 15, 5)->nullable();
$table->decimal('total_cbm', 15, 5)->nullable();
$table->decimal('kg', 15, 3)->nullable();
$table->decimal('total_kg', 15, 3)->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('loading_list_items');
}
};

View File

@@ -0,0 +1,31 @@
<?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::create('container_rows', function (Blueprint $table) {
$table->id();
$table->foreignId('container_id')
->constrained('containers')
->onDelete('cascade');
// Excel मधल्या row क्रमांकासाठी (optional)
$table->unsignedInteger('row_index')->nullable();
// या row चा full data: "heading text" => "cell value"
$table->json('data');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('container_rows');
}
};

View File

@@ -0,0 +1,24 @@
<?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('containers', function (Blueprint $table) {
$table->string('status', 20)
->default('pending')
->after('container_date');
});
}
public function down(): void
{
Schema::table('containers', function (Blueprint $table) {
$table->dropColumn('status');
});
}
};

View File

@@ -0,0 +1,43 @@
<?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('invoices', function (Blueprint $table) {
// 1) order_id foreign key काढा
$table->dropForeign(['order_id']);
// 2) order_id column काढा
$table->dropColumn('order_id');
// 3) container_id add करा
$table->unsignedBigInteger('container_id')->nullable()->after('id');
// 4) container_id FK
$table->foreign('container_id')
->references('id')
->on('containers')
->onDelete('cascade');
});
}
public function down(): void
{
Schema::table('invoices', function (Blueprint $table) {
// rollback: container_id काढून order_id परत add
$table->dropForeign(['container_id']);
$table->dropColumn('container_id');
$table->unsignedBigInteger('order_id')->index();
$table->foreign('order_id')
->references('id')
->on('orders')
->onDelete('cascade');
});
}
};

2525
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -1 +1,6 @@
import './bootstrap';
import "./bootstrap";
// VERY IMPORTANT — Load Echo globally
import "./echo";
console.log("[APP] app.js loaded");

View File

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

31
resources/js/echo.js Normal file
View File

@@ -0,0 +1,31 @@
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
// Get CSRF token from meta tag
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 8080,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 8080,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'http') === 'https',
enabledTransports: ['ws', 'wss'],
authEndpoint: '/admin/broadcasting/auth',
// ⭐ MOST IMPORTANT ⭐
withCredentials: true,
auth: {
headers: {
'X-CSRF-TOKEN': csrfToken,
'Accept': 'application/json',
}
}
});
console.log('%c[ECHO] Initialized!', 'color: green; font-weight: bold;', window.Echo);

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,169 @@
@extends('admin.layouts.app')
@section('page-title', 'Dashboard')
@section('page-title', 'Chat Support')
@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>
</div>
<div class="container py-4">
<h2 class="mb-4 fw-bold">Customer Support Chat</h2>
<div class="card shadow-sm">
<div class="card-body p-0">
@if($tickets->count() === 0)
<div class="p-4 text-center text-muted">
<h5>No customer chats yet.</h5>
</div>
@else
<ul class="list-group list-group-flush">
@foreach($tickets as $ticket)
@php
// Get last message
$lastMsg = $ticket->messages()->latest()->first();
@endphp
<li class="list-group-item py-3">
<div class="d-flex align-items-center justify-content-between">
<!-- Left side: User info + last message -->
<div class="d-flex align-items-center gap-3">
<!-- Profile Circle -->
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center"
style="width: 45px; height: 45px; font-size: 18px;">
{{ strtoupper(substr($ticket->user->customer_name ?? $ticket->user->name, 0, 1)) }}
</div>
<div>
<!-- Customer Name -->
<h6 class="mb-1 fw-semibold">
{{ $ticket->user->customer_name ?? $ticket->user->name }}
</h6>
<!-- Last message preview -->
<small class="text-muted">
@if($lastMsg)
@if($lastMsg->message)
{{ Str::limit($lastMsg->message, 35) }}
@elseif($lastMsg->file_type === 'image')
📷 Image
@elseif($lastMsg->file_type === 'video')
🎥 Video
@else
📎 Attachment
@endif
@else
<i>No messages yet</i>
@endif
</small>
</div>
</div>
<!-- Right Side: Status + Button -->
<div class="text-end">
<!-- Ticket Status -->
<span class="badge
{{ $ticket->status === 'open' ? 'bg-success' : 'bg-danger' }}">
{{ ucfirst($ticket->status) }}
</span>
<!-- Open Chat Button -->
<a href="{{ route('admin.chat.open', $ticket->id) }}"
class="btn btn-sm btn-primary ms-2">
Open Chat
</a>
</div>
</div>
</li>
@endforeach
</ul>
@endif
</div>
</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

View File

@@ -0,0 +1,200 @@
@extends('admin.layouts.app')
@section('page-title', 'Chat With ' . ($ticket->user->customer_name ?? $ticket->user->name))
@section('content')
<style>
.chat-box {
height: 70vh;
overflow-y: auto;
background: #f5f6fa;
border-radius: 8px;
padding: 15px;
}
.message {
max-width: 65%;
padding: 10px 14px;
border-radius: 15px;
margin-bottom: 10px;
font-size: 0.9rem;
line-height: 1.4;
}
.message.admin {
background: #007bff;
color: white;
margin-left: auto;
border-bottom-right-radius: 0;
}
.message.user {
background: #ffffff;
border: 1px solid #ddd;
margin-right: auto;
border-bottom-left-radius: 0;
}
.chat-input {
position: fixed;
bottom: 15px;
left: 250px;
right: 20px;
}
</style>
<div class="container py-4">
<div class="d-flex align-items-center mb-3">
<h4 class="fw-bold mb-0">
Chat With: {{ $ticket->user->customer_name ?? $ticket->user->name }}
</h4>
<span class="badge ms-3 {{ $ticket->status === 'open' ? 'bg-success' : 'bg-danger' }}">
{{ ucfirst($ticket->status) }}
</span>
</div>
<div id="chatBox" class="chat-box border shadow-sm">
@foreach($messages as $msg)
<div class="message {{ $msg->sender_type === 'App\\Models\\Admin' ? 'admin' : 'user' }}">
{{-- TEXT --}}
@if($msg->message)
<div>{{ $msg->message }}</div>
@endif
{{-- FILE --}}
@if($msg->file_path)
<div class="mt-2">
@php $isImage = Str::startsWith($msg->file_type, 'image'); @endphp
@if($isImage)
<img src="{{ asset('storage/'.$msg->file_path) }}" style="max-width:150px;" class="rounded">
@else
<a href="{{ asset('storage/'.$msg->file_path) }}" target="_blank">📎 View Attachment</a>
@endif
</div>
@endif
<small class="text-muted d-block mt-1">
{{ $msg->created_at->format('d M h:i A') }}
</small>
</div>
@endforeach
</div>
<div class="chat-input">
<div class="card shadow-sm">
<div class="card-body d-flex align-items-center gap-2">
<input type="text" id="messageInput" class="form-control" placeholder="Type your message...">
<input type="file" id="fileInput" class="form-control" style="max-width:200px;">
<button class="btn btn-primary" id="sendBtn">Send</button>
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
console.log("CHAT WINDOW: script loaded");
// -------------------------------
// WAIT FOR ECHO READY
// -------------------------------
function waitForEcho(callback, retries = 40) {
if (window.Echo) {
console.log("%c[ECHO] Ready!", "color: green; font-weight: bold;", window.Echo);
return callback();
}
console.warn("[ECHO] Not ready. Retrying...");
if (retries <= 0) {
console.error("[ECHO] FAILED to initialize after retry limit");
return;
}
setTimeout(() => waitForEcho(callback, retries - 1), 200);
}
// Scroll chat down
function scrollToBottom() {
const el = document.getElementById("chatBox");
if (el) el.scrollTop = el.scrollHeight;
}
scrollToBottom();
// -------------------------------
// SEND MESSAGE (WORKING PART FROM SCRIPT #1)
// -------------------------------
document.getElementById("sendBtn").addEventListener("click", function () {
console.log("[SEND] Attempting to send message...");
let msg = document.getElementById("messageInput").value;
let file = document.getElementById("fileInput").files[0];
if (!msg.trim() && !file) {
alert("Please type something or upload a file.");
return;
}
let formData = new FormData();
formData.append("message", msg);
if (file) formData.append("file", file);
fetch("{{ route('admin.chat.send', $ticket->id) }}", {
method: "POST",
headers: { "X-CSRF-TOKEN": "{{ csrf_token() }}" },
body: formData
})
.then(res => res.json())
.then((response) => {
console.log("[SEND] Message sent:", response);
document.getElementById("messageInput").value = "";
document.getElementById("fileInput").value = "";
})
.catch(err => console.error("[SEND] Error:", err));
});
// -------------------------------
// LISTEN FOR REALTIME MESSAGE (WORKING PART FROM SCRIPT #2)
// -------------------------------
waitForEcho(() => {
const ticketId = "{{ $ticket->id }}";
console.log("[ECHO] Subscribing to channel:", `ticket.${ticketId}`);
window.Echo.private(`ticket.${ticketId}`)
.listen("NewChatMessage", (event) => {
console.log("%c[REALTIME RECEIVED]", "color: blue; font-weight: bold;", event);
const msg = event.message;
let html = `
<div class="message ${msg.sender_type === 'App\\Models\\Admin' ? 'admin' : 'user'}">
${msg.message ?? ''}
`;
if (msg.file_url) {
if (msg.file_type.startsWith("image")) {
html += `<img src="${msg.file_url}" class="rounded mt-2" style="max-width:150px;">`;
} else {
html += `<a href="${msg.file_url}" target="_blank" class="mt-2 d-block">📎 View File</a>`;
}
}
html += `
<small class="text-muted d-block mt-1">Just now</small>
</div>
`;
document.getElementById("chatBox").innerHTML += html;
scrollToBottom();
});
});
</script>
@endsection

View File

@@ -0,0 +1,710 @@
@extends('admin.layouts.app')
@section('page-title', 'Containers')
@section('content')
<style>
:root {
--primary-color: #4c6fff;
--primary-gradient: linear-gradient(135deg, #4c6fff, #8e54e9);
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;
--info-color: #3b82f6;
--light-bg: #f8fafc;
--dark-text: #1e293b;
--gray-text: #64748b;
--border-color: #e2e8f0;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.1);
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1);
--shadow-lg: 0 10px 25px -5px rgba(0,0,0,0.1);
--radius-lg: 16px;
--radius-md: 12px;
--radius-sm: 8px;
}
.containers-wrapper {
min-height: calc(100vh - 180px);
padding: 20px 15px;
background: linear-gradient(135deg, #f6f9ff 0%, #f0f4ff 100%);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding: 0 10px;
}
.header-content h1 {
font-size: 28px;
font-weight: 700;
background: var(--primary-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 5px;
}
.header-subtitle {
color: var(--gray-text);
font-size: 14px;
font-weight: 500;
}
.add-container-btn {
background: var(--primary-gradient);
color: white;
border: none;
padding: 12px 28px;
border-radius: var(--radius-md);
font-weight: 600;
font-size: 14px;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 12px rgba(76, 111, 255, 0.3);
}
.add-container-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(76, 111, 255, 0.4);
color: white;
}
.add-container-btn i {
font-size: 16px;
}
.filter-card {
background: white;
border-radius: var(--radius-lg);
padding: 24px;
margin-bottom: 30px;
box-shadow: var(--shadow-lg);
border: 1px solid rgba(255,255,255,0.9);
backdrop-filter: blur(10px);
}
.filter-title {
font-size: 16px;
font-weight: 600;
color: var(--dark-text);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.filter-title i {
color: var(--primary-color);
}
.filter-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.filter-group {
position: relative;
}
.filter-group label {
display: block;
font-size: 12px;
font-weight: 600;
color: var(--gray-text);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.filter-input, .filter-select, .filter-date {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
font-size: 14px;
color: var(--dark-text);
background: white;
transition: all 0.3s ease;
}
.filter-input:focus, .filter-select:focus, .filter-date:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(76, 111, 255, 0.1);
}
.filter-input::placeholder {
color: #94a3b8;
}
.filter-actions {
display: flex;
gap: 10px;
align-items: flex-end;
}
.apply-btn {
background: var(--primary-gradient);
color: white;
border: none;
padding: 12px 24px;
border-radius: var(--radius-md);
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
min-height: 46px;
width: 100%;
}
.apply-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(76, 111, 255, 0.3);
}
.reset-btn {
background: white;
color: var(--gray-text);
border: 2px solid var(--border-color);
padding: 12px 24px;
border-radius: var(--radius-md);
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
min-height: 46px;
width: 100%;
}
.reset-btn:hover {
border-color: var(--primary-color);
color: var(--primary-color);
}
.main-card {
background: white;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-lg);
margin-bottom: 30px;
border: 1px solid rgba(255,255,255,0.9);
}
.card-header {
padding: 24px;
background: linear-gradient(135deg, #4c6fff, #8e54e9);
color: white;
}
.card-header h2 {
font-size: 20px;
font-weight: 700;
color: white;
margin: 0;
display: flex;
align-items: center;
gap: 12px;
}
.card-header h2 i {
color: white;
}
.stats-badge {
background: rgba(255, 255, 255, 0.2);
color: white;
font-size: 12px;
font-weight: 600;
padding: 4px 12px;
border-radius: 20px;
margin-left: 10px;
backdrop-filter: blur(10px);
}
.container-item {
padding: 16px 20px;
border-bottom: 1px solid var(--border-color);
transition: all 0.3s ease;
background: white;
}
.container-item:hover {
background: #f8fafc;
transform: translateX(4px);
}
.container-item:last-child {
border-bottom: none;
}
.container-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.container-info {
display: flex;
align-items: center;
gap: 12px;
}
.container-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--primary-gradient);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
font-weight: 700;
flex-shrink: 0;
box-shadow: 0 4px 12px rgba(76, 111, 255, 0.3);
}
.container-details h3 {
font-size: 16px;
font-weight: 700;
color: var(--dark-text);
margin: 0 0 4px 0;
}
.container-meta {
display: flex;
align-items: center;
gap: 12px;
font-size: 12px;
color: var(--gray-text);
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
}
.meta-item i {
font-size: 12px;
color: #94a3b8;
}
.status-badge {
padding: 6px 16px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.3px;
display: inline-flex;
align-items: center;
gap: 6px;
}
.status-badge i {
font-size: 10px;
}
.status-pending {
background: #fef3c7;
color: #d97706;
}
.status-in-progress {
background: #dbeafe;
color: #1d4ed8;
}
.status-completed {
background: #d1fae5;
color: #065f46;
}
.status-cancelled {
background: #fee2e2;
color: #991b1b;
}
.action-buttons {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.action-btn {
padding: 8px 16px;
border-radius: var(--radius-md);
font-size: 13px;
font-weight: 600;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.3s ease;
text-decoration: none;
}
.view-btn {
background: #e0f2fe;
color: #0369a1;
border: none;
}
.view-btn:hover {
background: #0ea5e9;
color: white;
}
.delete-btn {
background: #fef2f2;
color: #dc2626;
border: 1px solid #fecaca;
}
.delete-btn:hover {
background: #dc2626;
color: white;
}
.update-form {
display: flex;
align-items: center;
gap: 8px;
}
.status-select {
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
font-size: 13px;
color: var(--dark-text);
background: white;
min-width: 140px;
cursor: pointer;
}
.update-btn {
background: var(--primary-color);
color: white;
border: none;
padding: 8px 16px;
border-radius: var(--radius-md);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
}
.update-btn:hover {
background: #3b5de6;
}
.no-results {
text-align: center;
padding: 60px 20px;
}
.no-results-icon {
font-size: 64px;
color: var(--border-color);
margin-bottom: 20px;
}
.no-results h4 {
font-size: 18px;
color: var(--gray-text);
margin-bottom: 10px;
}
.no-results p {
color: #94a3b8;
font-size: 14px;
max-width: 400px;
margin: 0 auto;
}
.success-message {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
padding: 16px 24px;
border-radius: var(--radius-md);
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: var(--shadow-md);
animation: slideIn 0.3s ease;
}
.success-message i {
font-size: 20px;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
/* 🔥 Totals section */
.totals-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
margin-top: 16px;
padding: 16px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: var(--radius-md);
border-left: 4px solid var(--primary-color);
}
.total-card {
text-align: center;
padding: 12px;
background: white;
border-radius: var(--radius-sm);
box-shadow: var(--shadow-sm);
transition: all 0.3s ease;
}
.total-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.total-value {
font-size: 20px;
font-weight: 800;
color: var(--primary-color);
margin-bottom: 4px;
}
.total-label {
font-size: 11px;
font-weight: 600;
color: var(--gray-text);
text-transform: uppercase;
letter-spacing: 0.5px;
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.add-container-btn {
width: 100%;
justify-content: center;
}
.filter-grid {
grid-template-columns: 1fr;
}
.container-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.action-buttons {
width: 100%;
justify-content: flex-start;
}
}
@media (max-width: 576px) {
.update-form {
flex-direction: column;
width: 100%;
}
.status-select, .update-btn {
width: 100%;
}
}
</style>
<div class="containers-wrapper">
<div class="page-header">
<div class="header-content">
<h1>Container Management</h1>
<div class="header-subtitle">
Manage all containers, track status, and view entries in real-time
</div>
</div>
<a href="{{ route('containers.create') }}" class="add-container-btn">
<i class="fas fa-plus-circle"></i>
Add New Container
</a>
</div>
@if(session('success'))
<div class="success-message">
<i class="fas fa-check-circle"></i>
<span>{{ session('success') }}</span>
</div>
@endif
<div class="filter-card">
<div class="filter-title">
<i class="fas fa-filter"></i>
Filter Containers
</div>
<form method="GET" class="filter-grid">
<div class="filter-group">
<label><i class="fas fa-search"></i> Search</label>
<input type="text" name="search" class="filter-input"
placeholder="Search by container name or number..."
value="{{ request('search') }}">
</div>
<div class="filter-group">
<label><i class="fas fa-tag"></i> Status</label>
<select name="status" class="filter-select">
<option value="">All Status</option>
<option value="pending" {{ request('status') == 'pending' ? 'selected' : '' }}>Pending</option>
<option value="in-progress" {{ request('status') == 'in-progress' ? 'selected' : '' }}>In Progress</option>
<option value="completed" {{ request('status') == 'completed' ? 'selected' : '' }}>Completed</option>
<option value="cancelled" {{ request('status') == 'cancelled' ? 'selected' : '' }}>Cancelled</option>
</select>
</div>
<div class="filter-group">
<label><i class="fas fa-calendar"></i> Date</label>
<input type="date" name="date" class="filter-date" value="{{ request('date') }}">
</div>
<div class="filter-actions">
<button type="submit" class="apply-btn">
<i class="fas fa-search"></i> Apply Filters
</button>
<a href="{{ route('containers.index') }}" class="reset-btn">
<i class="fas fa-redo"></i> Reset
</a>
</div>
</form>
</div>
<div class="main-card">
<div class="card-header">
<h2>
<i class="fas fa-boxes"></i>
Containers List
<span class="stats-badge">{{ $containers->count() }} containers</span>
</h2>
</div>
@if($containers->isEmpty())
<div class="no-results">
<div class="no-results-icon">
<i class="fas fa-box-open"></i>
</div>
<h4>No containers found</h4>
<p>Get started by creating your first container</p>
</div>
@else
@foreach($containers as $container)
@php
$status = $container->status;
$statusClass = match ($status) {
'completed' => 'status-completed',
'in-progress' => 'status-in-progress',
'cancelled' => 'status-cancelled',
default => 'status-pending',
};
@endphp
<div class="container-item">
<div class="container-header">
<div class="container-info">
<div class="container-avatar">
{{ substr($container->container_name, 0, 2) }}
</div>
<div class="container-details">
<h3>{{ $container->container_name }}</h3>
<div class="container-meta">
<div class="meta-item">
<i class="fas fa-hashtag"></i>
<span>{{ $container->container_number }}</span>
</div>
<div class="meta-item">
<i class="fas fa-calendar"></i>
<span>{{ $container->container_date?->format('M d, Y') ?: 'No date' }}</span>
</div>
<div class="meta-item">
<i class="fas fa-list"></i>
<span>{{ $container->rows->count() }} entries</span>
</div>
</div>
</div>
</div>
<div class="action-buttons">
<span class="status-badge {{ $statusClass }}">
<i class="fas fa-circle"></i>
{{ ucfirst(str_replace('-', ' ', $status)) }}
</span>
<a href="{{ route('containers.show', $container->id) }}" class="action-btn view-btn">
<i class="fas fa-eye"></i> View
</a>
<form action="{{ route('containers.update-status', $container->id) }}"
method="POST" class="update-form">
@csrf
<select name="status" class="status-select">
<option value="pending" {{ $status === 'pending' ? 'selected' : '' }}>Pending</option>
<option value="in-progress" {{ $status === 'in-progress' ? 'selected' : '' }}>In Progress</option>
<option value="completed" {{ $status === 'completed' ? 'selected' : '' }}>Completed</option>
<option value="cancelled" {{ $status === 'cancelled' ? 'selected' : '' }}>Cancelled</option>
</select>
<button type="submit" class="update-btn">
<i class="fas fa-sync-alt"></i> Update
</button>
</form>
<form action="{{ route('containers.destroy', $container->id) }}" method="POST"
onsubmit="return confirm('Are you sure you want to delete this container and all its entries?');">
@csrf
@method('DELETE')
<button type="submit" class="action-btn delete-btn">
<i class="fas fa-trash"></i> Delete
</button>
</form>
</div>
</div>
<!-- 🔥 Totals instead of first row preview -->
<div class="totals-section">
<div class="total-card">
<div class="total-value">{{ number_format($container->summary['total_ctn'], 1) }}</div>
<div class="total-label">Total CTN</div>
</div>
<div class="total-card">
<div class="total-value">{{ number_format($container->summary['total_qty'], 0) }}</div>
<div class="total-label">Total QTY</div>
</div>
<div class="total-card">
<div class="total-value">{{ number_format($container->summary['total_cbm'], 3) }}</div>
<div class="total-label">Total CBM</div>
</div>
<div class="total-card">
<div class="total-value">{{ number_format($container->summary['total_kg'], 1) }}</div>
<div class="total-label">Total KG</div>
</div>
</div>
</div>
@endforeach
@endif
</div>
</div>
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
@endsection

View File

@@ -0,0 +1,251 @@
@extends('admin.layouts.app')
@section('page-title', 'Add Container')
@section('content')
<style>
.cm-add-wrapper {
padding: 10px 0 20px 0;
}
.cm-add-header-card {
border-radius: 14px;
border: none;
margin-bottom: 18px;
background: linear-gradient(90deg,#4c6fff,#8e54e9);
box-shadow: 0 6px 18px rgba(15,35,52,0.18);
color: #ffffff;
}
.cm-add-header-card .card-body {
padding: 14px 18px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.cm-add-title {
margin: 0;
font-size: 20px;
font-weight: 600;
}
.cm-add-sub {
font-size: 12px;
opacity: 0.9;
}
.cm-add-main-card {
border-radius: 14px;
border: none;
box-shadow: 0 6px 18px rgba(15,35,52,0.12);
}
.cm-add-main-card .card-header {
background:#ffffff;
border-bottom: 1px solid #edf0f5;
padding: 10px 18px;
}
.cm-add-main-card .card-header h5 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.cm-form-label {
font-size: 13px;
font-weight: 500;
color:#495057;
margin-bottom: 4px;
}
.cm-form-control {
font-size: 13px;
border-radius: 10px;
border:1px solid #d0d7e2;
padding: 8px 11px;
}
.cm-form-control:focus {
border-color:#4c6fff;
box-shadow:0 0 0 0.15rem rgba(76,111,255,.25);
}
.cm-help-text {
font-size: 11px;
color:#868e96;
margin-top: 2px;
}
.cm-btn-primary {
border-radius: 20px;
padding: 6px 22px;
font-size: 13px;
font-weight: 500;
}
.cm-btn-secondary {
border-radius: 20px;
padding: 6px 18px;
font-size: 13px;
}
.cm-error-list {
font-size: 13px;
}
</style>
<div class="container-fluid cm-add-wrapper">
{{-- TOP GRADIENT HEADER --}}
<div class="card cm-add-header-card">
<div class="card-body">
<div>
<h4 class="cm-add-title">Create New Container</h4>
<div class="cm-add-sub">
Add container details and upload Kent loading list Excel file.
</div>
</div>
<a href="{{ route('containers.index') }}" class="btn btn-light btn-sm">
Back to Containers
</a>
</div>
</div>
{{-- MAIN CARD --}}
<div class="card cm-add-main-card">
<div class="card-header">
<h5>Add Container</h5>
</div>
<div class="card-body">
{{-- SUCCESS MESSAGE --}}
@if (session('success'))
<div class="alert alert-success">
{{ session('success') }}
</div>
@endif
{{-- VALIDATION ERRORS --}}
@if ($errors->any())
<div class="alert alert-danger">
<ul class="mb-0 cm-error-list">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
{{-- UNMATCHED ROWS TABLE --}}
@if (session('unmatched_rows'))
<div class="alert alert-warning mt-3">
<strong>Mark number not matched:</strong>
@php
$unmatchedRows = session('unmatched_rows');
$headings = [];
if (!empty($unmatchedRows)) {
$headings = array_keys($unmatchedRows[0]['data'] ?? []);
// इथे Excel मधला 'MARK' कॉलम hide करतो, कारण आधीच Mark No वेगळा column आहे
$headings = array_filter($headings, function ($h) {
return strtoupper(trim($h)) !== 'MARK';
});
}
@endphp
@if(!empty($unmatchedRows))
<div class="table-responsive" style="max-height:260px; overflow:auto; border:1px solid #e3e6ef;">
<table class="table table-sm table-bordered mb-0" style="font-size:11.5px; min-width:800px;">
<thead class="table-light">
<tr>
<th>Excel Row</th>
<th>Mark No</th>
@foreach($headings as $head)
<th>{{ $head }}</th>
@endforeach
</tr>
</thead>
<tbody>
@foreach($unmatchedRows as $row)
<tr>
<td>{{ $row['excel_row'] }}</td>
<td>{{ $row['mark_no'] }}</td>
@foreach($headings as $head)
<td>{{ $row['data'][$head] ?? '' }}</td>
@endforeach
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
@endif
{{-- FORM: unmatched_rows असल्यावर form लपवायचा असेल तर खालील condition ठेवा --}}
@if (!session('unmatched_rows'))
<form action="{{ route('containers.store') }}" method="POST" enctype="multipart/form-data" class="mt-3">
@csrf
<div class="row g-3">
{{-- Container Name --}}
<div class="col-md-6">
<label class="cm-form-label">Container Name</label>
<input type="text"
name="container_name"
class="form-control cm-form-control"
value="{{ old('container_name') }}"
placeholder="Enter container name">
</div>
{{-- Container Number --}}
<div class="col-md-6">
<label class="cm-form-label">Container Number</label>
<input type="text"
name="container_number"
class="form-control cm-form-control"
value="{{ old('container_number') }}"
placeholder="Enter container number">
</div>
{{-- Container Date --}}
<div class="col-md-6">
<label class="cm-form-label">Container Date</label>
<input type="date"
name="container_date"
class="form-control cm-form-control"
value="{{ old('container_date') }}">
</div>
{{-- Excel File --}}
<div class="col-md-6">
<label class="cm-form-label">Loading List Excel</label>
<input type="file"
name="excel_file"
class="form-control cm-form-control"
accept=".xls,.xlsx">
<div class="cm-help-text">
Upload Kent loading list Excel file (.xls / .xlsx).
</div>
</div>
</div>
<div class="mt-4 d-flex gap-2">
<button type="submit" class="btn btn-primary cm-btn-primary">
Save Container
</button>
<a href="{{ route('containers.index') }}" class="btn btn-outline-secondary cm-btn-secondary">
Cancel
</a>
</div>
</form>
@endif
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,291 @@
@extends('admin.layouts.app')
@section('page-title', 'Container Details')
@section('content')
<style>
.cm-detail-wrapper {
padding: 10px 0 20px 0;
}
.cm-detail-header-card {
border-radius: 14px;
border: none;
margin-bottom: 18px;
background: linear-gradient(90deg,#4c6fff,#8e54e9);
box-shadow: 0 6px 18px rgba(15,35,52,0.18);
color:#ffffff;
}
.cm-detail-header-card .card-body {
padding: 14px 18px;
display:flex;
justify-content:space-between;
align-items:center;
gap:10px;
}
.cm-detail-title {
margin:0;
font-size:20px;
font-weight:600;
}
.cm-detail-sub {
font-size:12px;
opacity:0.9;
}
.cm-detail-main-card {
border-radius:14px;
border:none;
box-shadow:0 6px 18px rgba(15,35,52,0.12);
overflow:hidden;
}
.cm-detail-main-card .card-header {
background:#ffffff;
border-bottom:1px solid #edf0f5;
padding:10px 18px;
display:flex;
justify-content:space-between;
align-items:center;
gap:10px;
}
.cm-detail-main-card .card-header h5 {
margin:0;
font-size:16px;
font-weight:600;
}
.cm-info-label {
font-size:12px;
color:#6c757d;
font-weight:500;
}
.cm-info-value {
font-size:13px;
font-weight:500;
color:#343a40;
}
.cm-table-wrapper {
position:relative;
max-height: 520px;
overflow:auto;
border-top:1px solid #edf0f5;
}
.cm-table {
font-size:11.5px;
min-width: 1100px;
}
.cm-table thead th {
position: sticky;
top: 0;
z-index: 2;
background: #fff7e0;
color:#495057;
font-weight:600;
border-bottom:1px solid #e0d2a4;
white-space:nowrap;
}
.cm-table tbody tr:nth-child(even) {
background:#fafbff;
}
.cm-table tbody tr:hover {
background:#e9f3ff;
}
.cm-table td,
.cm-table th {
padding:4px 6px;
vertical-align:middle;
}
.cm-table td {
white-space:nowrap;
}
.cm-table-caption {
font-size:11px;
color:#868e96;
padding:6px 18px 0 18px;
}
.cm-filter-bar {
padding:8px 18px 0 18px;
display:flex;
justify-content:space-between;
align-items:center;
gap:10px;
flex-wrap:wrap;
}
.cm-filter-input {
max-width:240px;
font-size:12px;
border-radius:20px;
padding:6px 10px;
}
.cm-edit-save-btn {
font-size:12px;
border-radius:20px;
padding:6px 14px;
}
.cm-cell-input {
width: 140px;
min-width: 120px;
max-width: 220px;
font-size:11px;
padding:3px 4px;
height: 26px;
}
@media (max-width: 767px) {
.cm-detail-header-card .card-body {
flex-direction:column;
align-items:flex-start;
}
.cm-table-wrapper {
max-height:400px;
}
}
</style>
<div class="container-fluid cm-detail-wrapper">
{{-- TOP GRADIENT HEADER --}}
<div class="card cm-detail-header-card">
<div class="card-body">
<div>
<h4 class="cm-detail-title">
Container: {{ $container->container_number }}
</h4>
<div class="cm-detail-sub">
Edit loading list directly scroll horizontally and vertically like Excel.
</div>
</div>
<a href="{{ route('containers.index') }}" class="btn btn-light btn-sm">
Back to list
</a>
</div>
</div>
{{-- MAIN CARD --}}
<div class="card cm-detail-main-card">
<div class="card-header">
<h5>Container Information</h5>
@if(!$container->rows->isEmpty())
{{-- Save button (submits form below) --}}
<button type="submit"
form="cm-edit-rows-form"
class="btn btn-primary cm-edit-save-btn">
💾 Save Changes
</button>
@endif
</div>
<div class="card-body pb-0">
{{-- BASIC INFO --}}
<div class="row g-3 mb-2">
<div class="col-md-4">
<div class="cm-info-label">Container</div>
<div class="cm-info-value">{{ $container->container_name }}</div>
</div>
<div class="col-md-4">
<div class="cm-info-label">Date</div>
<div class="cm-info-value">
{{ $container->container_date?->format('d-m-Y') }}
</div>
</div>
<div class="col-md-4">
<div class="cm-info-label">Excel File</div>
@if($container->excel_file)
<div class="cm-info-value">
<a href="{{ \Illuminate\Support\Facades\Storage::url($container->excel_file) }}"
target="_blank">
Download / View Excel
</a>
</div>
@else
<div class="cm-info-value text-muted">Not uploaded</div>
@endif
</div>
</div>
</div>
@if($container->rows->isEmpty())
<div class="p-3">
<p class="mb-0">No entries found for this container.</p>
</div>
@else
@php
// सर्व headings collect
$allHeadings = [];
foreach ($container->rows as $row) {
if (is_array($row->data)) {
$allHeadings = array_unique(array_merge($allHeadings, array_keys($row->data)));
}
}
@endphp
{{-- FILTER BAR --}}
<div class="cm-filter-bar">
<div class="cm-table-caption">
Total rows: {{ $container->rows->count() }} Type to filter rows, edit cells then click "Save Changes".
</div>
<input type="text" id="cmRowSearch" class="form-control cm-filter-input"
placeholder="Quick search in table..." onkeyup="cmFilterRows()">
</div>
{{-- EDITABLE TABLE FORM --}}
<form id="cm-edit-rows-form"
action="{{ route('containers.rows.update', $container->id) }}"
method="POST">
@csrf
<div class="cm-table-wrapper mt-1">
<table class="table table-bordered table-hover cm-table" id="cmExcelTable">
<thead>
<tr>
@foreach($allHeadings as $heading)
<th>{{ $heading }}</th>
@endforeach
</tr>
</thead>
<tbody>
@foreach($container->rows as $row)
<tr>
@foreach($allHeadings as $heading)
@php
$value = $row->data[$heading] ?? '';
@endphp
<td>
<input type="text"
class="form-control form-control-sm cm-cell-input"
name="rows[{{ $row->id }}][{{ $heading }}]"
value="{{ $value }}">
</td>
@endforeach
</tr>
@endforeach
</tbody>
</table>
</div>
</form>
@endif
</div>
</div>
{{-- SIMPLE FRONTEND SEARCH --}}
<script>
function cmFilterRows() {
const input = document.getElementById('cmRowSearch');
if (!input) return;
const filter = input.value.toLowerCase();
const table = document.getElementById('cmExcelTable');
if (!table) return;
const rows = table.getElementsByTagName('tr');
for (let i = 1; i < rows.length; i++) { // skip header
const cells = rows[i].getElementsByTagName('td');
let match = false;
for (let j = 0; j < cells.length; j++) {
const txt = cells[j].textContent || cells[j].innerText;
if (txt.toLowerCase().indexOf(filter) > -1) {
match = true;
break;
}
}
rows[i].style.display = match ? '' : 'none';
}
}
</script>
@endsection

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,21 @@
<!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" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- CRITICAL: CSRF Token for Echo -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('page-title', 'Admin Panel')</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<style>
body {
@@ -20,7 +28,6 @@
transition: all 0.3s ease-in-out;
}
/* ✨ Sidebar Glass + Animated Highlight Effect */
.sidebar {
width: 200px;
height: 100vh;
@@ -39,7 +46,6 @@
left: 0;
}
/* Sidebar collapsed state */
.sidebar.collapsed {
transform: translateX(-100%);
opacity: 0;
@@ -62,10 +68,11 @@
}
.sidebar .word {
color: #800000; font-size: 13px; line-height: 1.24;
color: #800000;
font-size: 13px;
line-height: 1.24;
}
/* 🔥 Sidebar Links */
.sidebar a {
display: flex;
align-items: center;
@@ -82,7 +89,6 @@
z-index: 0;
}
/* Background Animation */
.sidebar a::before {
content: "";
position: absolute;
@@ -107,7 +113,6 @@
color: #1258e0 !important;
}
/* Icon bounce effect */
.sidebar a i {
margin-right: 8px;
font-size: 1.1rem;
@@ -119,7 +124,6 @@
color: #1258e0 !important;
}
/* Active link glow effect */
.sidebar a.active {
background: linear-gradient(90deg, rgba(80,120,255,0.15), rgba(120,180,255,0.2));
color: #1258e0 !important;
@@ -134,7 +138,6 @@
opacity: 0.2;
}
/* Logout Button */
.sidebar form button {
border-radius: 12px;
margin-top: 12px;
@@ -148,7 +151,6 @@
transform: scale(1.03);
}
/* 🧭 Main Content */
.main-content {
flex-grow: 1;
min-height: 100vh;
@@ -160,13 +162,11 @@
transition: all 0.3s ease-in-out;
}
/* Main content when sidebar is collapsed */
.main-content.expanded {
margin-left: 0;
width: 100vw;
}
/* Header hamburger button */
.header-toggle {
background: transparent;
border: none;
@@ -199,7 +199,7 @@
background: #fff;
padding: 10px 18px !important;
position: relative;
height: 48px;
height: 65px;
width: 100%;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
@@ -219,95 +219,136 @@
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">
<div class="logo">
<img src="{{ asset('images/kent_logo2.png') }}" alt="Kent Logo">
<div class="word"><strong>KENT</strong><br /><small>International Pvt. Ltd.</small></div>
<div class="word">
<strong>KENT</strong><br />
<small>International Pvt. Ltd.</small>
</div>
</div>
{{-- Dashboard (requires order.view) --}}
@can('order.view')
<a href="{{ route('admin.dashboard') }}" class="{{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
<i class="bi bi-house"></i> Dashboard
</a>
<a href="{{ route('admin.dashboard') }}" class="{{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
<i class="bi bi-house"></i> Dashboard
</a>
@endcan
<!--
{{-- Shipments --}}
@can('shipment.view')
<a href="{{ route('admin.shipments') }}" class="{{ request()->routeIs('admin.shipments') ? 'active' : '' }}">
<i class="bi bi-truck"></i> Shipments
</a>
<a href="{{ route('admin.shipments') }}" class="{{ request()->routeIs('admin.shipments') ? 'active' : '' }}">
<i class="bi bi-truck"></i> Shipments
</a>
@endcan -->
{{-- Container NEW MENU --}}
@can('container.view')
<a href="{{ route('containers.index') }}" class="{{ request()->routeIs('containers.*') ? 'active' : '' }}">
<i class="fa-solid fa-box"></i> Container
</a>
@endcan
{{-- Invoice --}}
@can('invoice.view')
<a href="{{ route('admin.invoices.index') }}" class="{{ request()->routeIs('admin.invoices.index') ? 'active' : '' }}">
<i class="bi bi-receipt"></i> Invoice
</a>
<a href="{{ route('admin.invoices.index') }}" class="{{ request()->routeIs('admin.invoices.index') ? 'active' : '' }}">
<i class="bi bi-receipt"></i> Invoice
</a>
@endcan
{{-- Customers --}}
@can('customer.view')
<a href="{{ route('admin.customers.index') }}" class="{{ request()->routeIs('admin.customers.index') ? 'active' : '' }}">
<i class="bi bi-people"></i> Customers
</a>
<a href="{{ route('admin.customers.index') }}" class="{{ request()->routeIs('admin.customers.index') ? 'active' : '' }}">
<i class="bi bi-people"></i> Customers
</a>
@endcan
{{-- Reports --}}
@can('report.view')
<a href="{{ route('admin.reports') }}" class="{{ request()->routeIs('admin.reports') ? 'active' : '' }}">
<i class="bi bi-graph-up"></i> Reports
</a>
<a href="{{ route('admin.reports') }}" class="{{ request()->routeIs('admin.reports') ? 'active' : '' }}">
<i class="bi bi-graph-up"></i> Reports
</a>
@endcan
{{-- Chat Support (NO PERMISSION REQUIRED) --}}
<a href="{{ route('admin.chat_support') }}" class="{{ request()->routeIs('admin.chat_support') ? 'active' : '' }}">
<i class="bi bi-chat-dots"></i> Chat Support
</a>
{{-- Chat Support --}}
@can('chat_support.view')
<a href="{{ route('admin.chat_support') }}" class="{{ request()->routeIs('admin.chat_support') ? 'active' : '' }}">
<i class="bi bi-chat-dots"></i> Chat Support
</a>
@endcan
{{-- Orders --}}
@can('orders.view')
<a href="{{ route('admin.orders') }}" class="{{ request()->routeIs('admin.orders') ? 'active' : '' }}">
<i class="bi bi-bag"></i> Orders
</a>
<a href="{{ route('admin.orders') }}" class="{{ request()->routeIs('admin.orders') ? 'active' : '' }}">
<i class="bi bi-bag"></i> Orders
</a>
@endcan
{{-- Requests --}}
@can('request.view')
<a href="{{ route('admin.requests') }}" class="{{ request()->routeIs('admin.requests') ? 'active' : '' }}">
<i class="bi bi-envelope"></i> Requests
</a>
<a href="{{ route('admin.requests') }}" class="{{ request()->routeIs('admin.requests') ? 'active' : '' }}">
<i class="bi bi-envelope"></i> Requests
</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' : '' }}">
<i class="bi bi-person-badge"></i> Staff
</a>
{{-- Staff --}}
@can('staff.view')
<a href="{{ route('admin.staff.index') }}" class="{{ request()->routeIs('admin.staff.*') ? 'active' : '' }}">
<i class="bi bi-person-badge"></i> Staff
</a>
@endcan
{{-- Account Section --}}
@can('account.view')
<a href="{{ route('admin.account') }}" class="{{ request()->routeIs('admin.account') ? 'active' : '' }}">
<i class="bi bi-gear"></i> Account
</a>
<a href="{{ route('admin.account') }}" class="{{ request()->routeIs('admin.account') ? 'active' : '' }}">
<i class="bi bi-gear"></i> Account
</a>
@endcan
{{-- Mark List --}}
@can('mark_list.view')
<a href="{{ route('admin.marklist.index') }}" class="{{ request()->routeIs('admin.marklist.index') ? 'active' : '' }}">
<i class="bi bi-list-check"></i> Mark List
</a>
<a href="{{ route('admin.marklist.index') }}" class="{{ request()->routeIs('admin.marklist.index') ? 'active' : '' }}">
<i class="bi bi-list-check"></i> Mark List
</a>
@endcan
</div>
<div class="main-content">
@@ -325,44 +366,53 @@
<div class="dropdown">
<a href="#" class="d-flex align-items-center text-decoration-none dropdown-toggle" data-bs-toggle="dropdown">
<img src="https://i.pravatar.cc/40" class="rounded-circle me-2" width="35" height="35">
<span class="dropdown-user-profile-name">{{ Auth::guard('admin')->user()->name ?? 'User' }}</span>
<span class="dropdown-user-profile-name">
{{ auth('admin')->user()->name ?? 'User' }}
</span>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{{ route('admin.profile') }}"><i class="bi bi-person-circle me-2"></i>Profile</a></li>
<li>
<a class="dropdown-item" href="{{ route('admin.profile') }}">
<i class="bi bi-person-circle me-2"></i>Profile
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<form method="POST" action="{{ route('admin.logout') }}">
@csrf
<button class="dropdown-item" type="submit"><i class="bi bi-box-arrow-right me-2"></i>Logout</button>
<button class="dropdown-item" type="submit">
<i class="bi bi-box-arrow-right me-2"></i>Logout
</button>
</form>
</li>
</ul>
</div>
</div>
</header>
<div class="content-wrapper">
@yield('content')
</div>
</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');
const sidebar = document.querySelector('.sidebar');
const mainContent = document.querySelector('.main-content');
// Function to toggle sidebar
function toggleSidebar() {
sidebar.classList.toggle('collapsed');
mainContent.classList.toggle('expanded');
}
// Header toggle button click event
if (headerToggle) {
headerToggle.addEventListener('click', toggleSidebar);
}
});
</script>
</body>
</html>
</html>

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

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