Add Container field

This commit is contained in:
Utkarsh Khedkar
2026-02-17 14:32:48 +05:30
parent 0a65d5f596
commit 94e211f87e
49 changed files with 8989 additions and 1290 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

@@ -0,0 +1,85 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\SupportTicket;
use App\Models\ChatMessage;
use App\Events\NewChatMessage;
use Illuminate\Http\Request;
class AdminChatController extends Controller
{
/**
* 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 2: Open chat window for a specific user
*/
public function openChat($ticketId)
{
$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)
*/
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,
];
// File Upload
if ($request->hasFile('file')) {
$path = $request->file('file')->store('chat', 'public');
$data['file_path'] = $path;
$data['file_type'] = $request->file('file')->getMimeType();
}
// Save message
$message = ChatMessage::create($data);
$message->load('sender');
\Log::info("DEBUG: ChatController sendMessage called", [
'ticket_id' => $ticketId,
'payload' => $request->all()
]);
// 🔥 LIVE CHAT - Queue bypass (100% working)
broadcast(new NewChatMessage($message))->toOthers();
return response()->json([
'success' => true,
'message' => $message
]);
}
}

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,13 +29,8 @@ class AdminInvoiceController extends Controller
// -------------------------------------------------------------
public function popup($id)
{
$invoice = Invoice::with(['items', 'order'])->findOrFail($id);
// Find actual Shipment record
$shipment = \App\Models\Shipment::whereHas('items', function ($q) use ($invoice) {
$q->where('order_id', $invoice->order_id);
})
->first();
$invoice = Invoice::with(['items', 'customer', 'container'])->findOrFail($id);
$shipment = null;
return view('admin.popup_invoice', compact('invoice', 'shipment'));
}
@@ -42,24 +40,28 @@ 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'));
}
// -------------------------------------------------------------
// 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',
@@ -70,31 +72,32 @@ class AdminInvoiceController extends Controller
'notes' => 'nullable|string',
]);
Log::info("✅ Validated Invoice Update Data", $data);
Log::info('✅ Validated Invoice Update Data', $data);
$finalAmount = floatval($data['final_amount']);
$taxPercent = floatval($data['tax_percent']);
$taxAmount = 0;
// 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;
$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", [
Log::info('📌 Final Calculated Invoice Values', [
'invoice_id' => $invoice->id,
'final_amount' => $finalAmount,
'gst_amount' => $data['gst_amount'],
@@ -105,13 +108,28 @@ class AdminInvoiceController extends Controller
'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()
@@ -119,13 +137,89 @@ class AdminInvoiceController extends Controller
->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/');
@@ -134,19 +228,30 @@ class AdminInvoiceController extends Controller
}
$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]);
}
// -------------------------------------------------------------
// INSTALLMENTS (ADD/DELETE)
// INSTALLMENTS (ADD)
// -------------------------------------------------------------
public function storeInstallment(Request $request, $invoice_id)
{
@@ -158,15 +263,13 @@ class AdminInvoiceController extends Controller
]);
$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.'
'message' => 'Installment amount exceeds remaining balance.',
], 422);
}
@@ -180,7 +283,6 @@ 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']);
}
@@ -194,9 +296,13 @@ class AdminInvoiceController extends Controller
'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
'isCompleted' => $newPaid >= $invoice->final_amount_with_gst,
]);
}
// -------------------------------------------------------------
// INSTALLMENTS (DELETE)
// -------------------------------------------------------------
public function deleteInstallment($id)
{
$installment = InvoiceInstallment::findOrFail($id);
@@ -207,8 +313,7 @@ class AdminInvoiceController extends Controller
$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']);
}
@@ -220,8 +325,7 @@ class AdminInvoiceController extends Controller
'finalAmountWithGst' => $invoice->final_amount_with_gst,
'baseAmount' => $invoice->final_amount,
'remaining' => $remaining,
'isZero' => $paidTotal == 0
'isZero' => $paidTotal == 0,
]);
}
}

View File

@@ -92,7 +92,7 @@ class AdminOrderController extends Controller
]);
//If you want to auto-create an invoice at order creation, uncomment:
$this->createInvoice($order);
// $this->createInvoice($order);
return redirect()->route('admin.orders.show', $order->id)
->with('success', 'Order created successfully.');
@@ -148,7 +148,7 @@ class AdminOrderController extends Controller
// recalc totals and save to order
$this->recalcTotals($order);
$this->updateInvoiceFromOrder($order); // <-- NEW
// $this->updateInvoiceFromOrder($order); // <-- NEW
return redirect()->back()->with('success', 'Item added and totals updated.');
}
@@ -594,70 +594,70 @@ class AdminOrderController extends Controller
// =======================
// 1. Auto-generate invoice number
$lastInvoice = \App\Models\Invoice::latest()->first();
$nextInvoice = $lastInvoice ? $lastInvoice->id + 1 : 1;
$invoiceNumber = 'INV-' . date('Y') . '-' . str_pad($nextInvoice, 6, '0', STR_PAD_LEFT);
// $lastInvoice = \App\Models\Invoice::latest()->first();
// $nextInvoice = $lastInvoice ? $lastInvoice->id + 1 : 1;
// $invoiceNumber = 'INV-' . date('Y') . '-' . str_pad($nextInvoice, 6, '0', STR_PAD_LEFT);
// 2. Fetch customer (using mark list → customer_id)
$markList = MarkList::where('mark_no', $order->mark_no)->first();
$customer = null;
// $markList = MarkList::where('mark_no', $order->mark_no)->first();
// $customer = null;
if ($markList && $markList->customer_id) {
$customer = \App\Models\User::where('customer_id', $markList->customer_id)->first();
}
// if ($markList && $markList->customer_id) {
// $customer = \App\Models\User::where('customer_id', $markList->customer_id)->first();
// }
// 3. Create Invoice Record
$invoice = \App\Models\Invoice::create([
'order_id' => $order->id,
'customer_id' => $customer->id ?? null,
'mark_no' => $order->mark_no,
// $invoice = \App\Models\Invoice::create([
// 'order_id' => $order->id,
// 'customer_id' => $customer->id ?? null,
// 'mark_no' => $order->mark_no,
'invoice_number' => $invoiceNumber,
'invoice_date' => now(),
'due_date' => now()->addDays(10),
// 'invoice_number' => $invoiceNumber,
// 'invoice_date' => now(),
// 'due_date' => now()->addDays(10),
'payment_method' => null,
'reference_no' => null,
'status' => 'pending',
// 'payment_method' => null,
// 'reference_no' => null,
// 'status' => 'pending',
'final_amount' => $total_amount,
'gst_percent' => 0,
'gst_amount' => 0,
'final_amount_with_gst' => $total_amount,
// 'final_amount' => $total_amount,
// 'gst_percent' => 0,
// 'gst_amount' => 0,
// 'final_amount_with_gst' => $total_amount,
// snapshot customer fields
'customer_name' => $customer->customer_name ?? null,
'company_name' => $customer->company_name ?? null,
'customer_email' => $customer->email ?? null,
'customer_mobile' => $customer->mobile_no ?? null,
'customer_address' => $customer->address ?? null,
'pincode' => $customer->pincode ?? null,
// // snapshot customer fields
// 'customer_name' => $customer->customer_name ?? null,
// 'company_name' => $customer->company_name ?? null,
// 'customer_email' => $customer->email ?? null,
// 'customer_mobile' => $customer->mobile_no ?? null,
// 'customer_address' => $customer->address ?? null,
// 'pincode' => $customer->pincode ?? null,
'notes' => null,
]);
// 'notes' => null,
// ]);
// 4. Clone order items into invoice_items
foreach ($order->items as $item) {
\App\Models\InvoiceItem::create([
'invoice_id' => $invoice->id,
'description' => $item->description,
'ctn' => $item->ctn,
'qty' => $item->qty,
'ttl_qty' => $item->ttl_qty,
'unit' => $item->unit,
'price' => $item->price,
'ttl_amount' => $item->ttl_amount,
'cbm' => $item->cbm,
'ttl_cbm' => $item->ttl_cbm,
'kg' => $item->kg,
'ttl_kg' => $item->ttl_kg,
'shop_no' => $item->shop_no,
]);
}
// foreach ($order->items as $item) {
// \App\Models\InvoiceItem::create([
// 'invoice_id' => $invoice->id,
// 'description' => $item->description,
// 'ctn' => $item->ctn,
// 'qty' => $item->qty,
// 'ttl_qty' => $item->ttl_qty,
// 'unit' => $item->unit,
// 'price' => $item->price,
// 'ttl_amount' => $item->ttl_amount,
// 'cbm' => $item->cbm,
// 'ttl_cbm' => $item->ttl_cbm,
// 'kg' => $item->kg,
// 'ttl_kg' => $item->ttl_kg,
// 'shop_no' => $item->shop_no,
// ]);
// }
// 5. TODO: PDF generation (I will add this later)
$invoice->pdf_path = null; // placeholder for now
$invoice->save();
// $invoice->pdf_path = null; // placeholder for now
// $invoice->save();
// =======================
// END INVOICE CREATION

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

@@ -0,0 +1,93 @@
<?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,
];
// Handle file upload
if ($request->hasFile('file')) {
$path = $request->file('file')->store('chat', 'public');
$data['file_path'] = $path;
$data['file_type'] = $request->file('file')->getMimeType();
}
// Save message
$message = ChatMessage::create($data);
// Load sender info for broadcast
$message->load('sender');
// Fire real-time event
broadcast(new NewChatMessage($message))->toOthers();
return response()->json([
'success' => true,
'message' => $message
]);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class ChatMessage extends Model
{
use HasFactory;
protected $fillable = [
'ticket_id',
'sender_id',
'sender_type', // user OR admin
'message',
'file_path',
'file_type',
];
/**
* 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,42 +9,31 @@ class Invoice extends Model
{
use HasFactory;
protected $fillable = [
'order_id',
'container_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',
'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
****************************/
@@ -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

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

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

1563
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,
],
];

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,
],
];

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

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);

View File

@@ -1,12 +1,94 @@
@extends('admin.layouts.app')
@section('page-title', 'Dashboard')
@section('page-title', 'Chat Support')
@section('content')
<div class="container py-4">
<h2 class="mb-4 fw-bold">Customer Support Chat</h2>
<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 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

View File

@@ -0,0 +1,201 @@
@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

View File

@@ -4,9 +4,6 @@
@section('content')
<style>
/* --------------------------------------------------
GLOBAL VARIABLES & THEME
-------------------------------------------------- */
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--success-gradient: linear-gradient(135deg, #10b981 0%, #059669 100%);
@@ -20,22 +17,22 @@
--border-radius: 12px;
}
/* --------------------------------------------------
GLOBAL STYLES & ANIMATIONS
-------------------------------------------------- */
body {
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
font-family: "Inter", "Segoe UI", system-ui, sans-serif;
}
@keyframes fadeUp {
0% { opacity: 0; transform: translateY(20px) scale(0.98); }
100% { opacity: 1; transform: translateY(0) scale(1); }
0% {
opacity: 0;
transform: translateY(20px) scale(0.98);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* --------------------------------------------------
COMPACT CARD DESIGN
-------------------------------------------------- */
.glass-card {
background: var(--glass-bg);
backdrop-filter: blur(20px);
@@ -52,16 +49,15 @@ body {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(45deg,
background: linear-gradient(
45deg,
rgba(102, 126, 234, 0.03),
rgba(118, 75, 162, 0.03) 50%,
rgba(16, 185, 129, 0.03));
rgba(16, 185, 129, 0.03)
);
pointer-events: none;
}
/* --------------------------------------------------
COMPACT CARD HEADER
-------------------------------------------------- */
.card-header-compact {
background: var(--primary-gradient);
color: #fff;
@@ -81,9 +77,6 @@ body {
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* --------------------------------------------------
COMPACT CARD BODY
-------------------------------------------------- */
.card-body-compact {
padding: 1.5rem;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
@@ -91,9 +84,6 @@ body {
z-index: 1;
}
/* --------------------------------------------------
COMPACT FORM ELEMENTS
-------------------------------------------------- */
.form-grid-compact {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
@@ -115,25 +105,24 @@ body {
gap: 8px;
}
.form-control-compact, .form-select-compact {
.form-control-compact,
.form-select-compact {
border: 2px solid #e2e8f0;
border-radius: 8px;
padding: 10px 12px;
font-size: 0.9rem;
transition: all 0.3s ease;
background: white;
background: #ffffff;
width: 100%;
}
.form-control-compact:focus, .form-select-compact:focus {
.form-control-compact:focus,
.form-select-compact:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
outline: none;
}
/* --------------------------------------------------
COMPACT BUTTONS
-------------------------------------------------- */
.btn-compact {
padding: 10px 20px;
border-radius: 8px;
@@ -183,9 +172,6 @@ body {
color: white;
}
/* --------------------------------------------------
COMPACT SUMMARY CARDS
-------------------------------------------------- */
.summary-grid-compact {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
@@ -194,7 +180,7 @@ body {
}
.summary-card-compact {
background: white;
background: #ffffff;
padding: 1rem;
border-radius: 8px;
box-shadow: var(--shadow-soft);
@@ -202,9 +188,17 @@ body {
text-align: center;
}
.summary-card-compact.total { border-left-color: #10b981; }
.summary-card-compact.paid { border-left-color: #3b82f6; }
.summary-card-compact.remaining { border-left-color: #f59e0b; }
.summary-card-compact.total {
border-left-color: #10b981;
}
.summary-card-compact.paid {
border-left-color: #3b82f6;
}
.summary-card-compact.remaining {
border-left-color: #f59e0b;
}
.summary-value-compact {
font-size: 1.5rem;
@@ -220,11 +214,8 @@ body {
letter-spacing: 0.5px;
}
/* --------------------------------------------------
COMPACT AMOUNT BREAKDOWN
-------------------------------------------------- */
.amount-breakdown-compact {
background: white;
background: #ffffff;
padding: 1rem;
border-radius: 8px;
box-shadow: var(--shadow-soft);
@@ -254,9 +245,6 @@ body {
font-size: 0.9rem;
}
/* --------------------------------------------------
COMPACT TABLE
-------------------------------------------------- */
.table-compact {
width: 100%;
border-collapse: collapse;
@@ -266,7 +254,7 @@ body {
.table-compact thead th {
background: var(--primary-gradient);
color: white;
color: #ffffff;
padding: 0.75rem 1rem;
font-weight: 600;
text-transform: uppercase;
@@ -276,7 +264,7 @@ body {
}
.table-compact tbody tr {
background: white;
background: #ffffff;
transition: all 0.2s ease;
}
@@ -290,9 +278,6 @@ body {
vertical-align: middle;
}
/* --------------------------------------------------
BADGE STYLES
-------------------------------------------------- */
.badge-compact {
padding: 0.25rem 0.75rem;
border-radius: 20px;
@@ -300,64 +285,53 @@ body {
font-weight: 600;
}
/* --------------------------------------------------
RESPONSIVE DESIGN
-------------------------------------------------- */
@media (max-width: 768px) {
.glass-card {
margin: 0.5rem;
border-radius: 10px;
}
.card-body-compact {
padding: 1rem;
}
.form-grid-compact {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.summary-grid-compact {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.btn-compact {
width: 100%;
justify-content: center;
}
.table-compact {
font-size: 0.8rem;
}
}
</style>
<!-- Main Container -->
<div class="container-fluid py-3">
<!-- Invoice Preview Section -->
{{-- Invoice Preview / Overview --}}
<div class="glass-card">
<div class="card-header-compact">
<h4>
<i class="fas fa-file-invoice me-2"></i>Invoice Overview
<i class="fas fa-file-invoice me-2"></i>
Invoice Overview
</h4>
</div>
<div class="card-body-compact">
@include('admin.popup_invoice', [
'invoice' => $invoice,
'shipment' => $shipment,
'embedded' => true
])
{{-- इथे popup_invoice मधूनच items table (editable) येईल --}}
@include('admin.popup_invoice', ['invoice' => $invoice, 'shipment' => $shipment, 'embedded' => true])
</div>
</div>
<!-- Edit Invoice Form -->
{{-- Edit Invoice Header Details (POST) --}}
<div class="glass-card">
<div class="card-header-compact">
<h4>
<i class="fas fa-edit me-2"></i>Edit Invoice Details
<i class="fas fa-edit me-2"></i>
Edit Invoice Details
</h4>
</div>
<div class="card-body-compact">
@@ -365,121 +339,146 @@ body {
@csrf
<div class="form-grid-compact">
<!-- Invoice Date -->
{{-- Invoice Date --}}
<div class="form-group-compact">
<label class="form-label-compact">
<i class="fas fa-calendar-day"></i>
Invoice Date
<i class="fas fa-calendar-day"></i> Invoice Date
</label>
<input type="date" class="form-control-compact" name="invoice_date"
value="{{ $invoice->invoice_date }}" required>
<input type="date"
name="invoice_date"
class="form-control-compact"
value="{{ old('invoice_date', $invoice->invoice_date) }}"
required>
</div>
<!-- Due Date -->
{{-- Due Date --}}
<div class="form-group-compact">
<label class="form-label-compact">
<i class="fas fa-clock"></i>
Due Date
<i class="fas fa-clock"></i> Due Date
</label>
<input type="date" class="form-control-compact" name="due_date"
value="{{ $invoice->due_date }}" required>
<input type="date"
name="due_date"
class="form-control-compact"
value="{{ old('due_date', $invoice->due_date) }}"
required>
</div>
<!-- Final Amount -->
{{-- Final Amount (Base) --}}
<div class="form-group-compact">
<label class="form-label-compact">
<i class="fas fa-money-bill-wave"></i>
Final Amount ()
<i class="fas fa-money-bill-wave"></i> Final Amount (Before GST)
</label>
<input type="number" step="0.01" class="form-control-compact" name="final_amount"
value="{{ $invoice->final_amount }}" required>
<input type="number"
step="0.01"
name="final_amount"
class="form-control-compact"
value="{{ old('final_amount', $invoice->final_amount) }}"
required>
</div>
<!-- Tax Type -->
{{-- Tax Type --}}
<div class="form-group-compact">
<label class="form-label-compact">
<i class="fas fa-receipt"></i>
Tax Type
<i class="fas fa-receipt"></i> Tax Type
</label>
<div class="d-flex gap-3 mt-1">
<div class="form-check">
<input class="form-check-input" type="radio" name="tax_type"
value="gst" @checked($invoice->tax_type === 'gst')>
<label class="form-check-label fw-semibold">GST</label>
<input class="form-check-input"
type="radio"
name="tax_type"
value="gst"
{{ old('tax_type', $invoice->tax_type) === 'gst' ? 'checked' : '' }}>
<label class="form-check-label fw-semibold">GST (CGST + SGST)</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="tax_type"
value="igst" @checked($invoice->tax_type === 'igst')>
<input class="form-check-input"
type="radio"
name="tax_type"
value="igst"
{{ old('tax_type', $invoice->tax_type) === 'igst' ? 'checked' : '' }}>
<label class="form-check-label fw-semibold">IGST</label>
</div>
</div>
</div>
<!-- Tax Percentage -->
{{-- Tax Percentage --}}
<div class="form-group-compact">
<label class="form-label-compact">
<i class="fas fa-percentage"></i>
Tax Percentage (%)
<i class="fas fa-percentage"></i> Tax Percentage
</label>
<input type="number" step="0.01" min="0" max="28" class="form-control-compact"
<input type="number"
step="0.01"
min="0"
max="28"
name="tax_percent"
value="{{ old('tax_percent', $invoice->tax_type == 'gst' ? $invoice->cgst_percent + $invoice->sgst_percent : $invoice->igst_percent) }}"
class="form-control-compact"
value="{{ old('tax_percent',
$invoice->tax_type === 'gst'
? ($invoice->cgst_percent + $invoice->sgst_percent)
: $invoice->igst_percent
) }}"
required>
</div>
<!-- Status -->
{{-- Status --}}
<div class="form-group-compact">
<label class="form-label-compact">
<i class="fas fa-tasks"></i>
Status
<i class="fas fa-tasks"></i> Status
</label>
<select class="form-select-compact" name="status" required>
<option value="pending" @selected($invoice->status=='pending')> Pending</option>
<option value="paid" @selected($invoice->status=='paid')> Paid</option>
<option value="overdue" @selected($invoice->status=='overdue')>⚠️ Overdue</option>
<select name="status" class="form-select-compact" required>
<option value="pending" {{ old('status', $invoice->status) === 'pending' ? 'selected' : '' }}>
Pending
</option>
<option value="paid" {{ old('status', $invoice->status) === 'paid' ? 'selected' : '' }}>
Paid
</option>
<option value="overdue" {{ old('status', $invoice->status) === 'overdue' ? 'selected' : '' }}>
Overdue
</option>
</select>
</div>
<!-- Notes -->
{{-- Notes --}}
<div class="form-group-compact" style="grid-column: 1 / -1;">
<label class="form-label-compact">
<i class="fas fa-sticky-note"></i>
Notes
<i class="fas fa-sticky-note"></i> Notes
</label>
<textarea class="form-control-compact" rows="3" name="notes"
placeholder="Add any additional notes...">{{ $invoice->notes }}</textarea>
<textarea name="notes"
rows="3"
class="form-control-compact"
placeholder="Add any additional notes...">{{ old('notes', $invoice->notes) }}</textarea>
</div>
</div>
<div class="text-end mt-3">
@can('invoice.edit')
<button type="submit" class="btn-success-compact btn-compact">
<i class="fas fa-save me-2"></i>Update Invoice
</button>
@endcan
</div>
</form>
</div>
</div>
@php
$totalPaid = $invoice->installments->sum('amount');
$totalPaid = $invoice->installments()->sum('amount');
$remaining = $invoice->final_amount_with_gst - $totalPaid;
@endphp
<!-- Amount Breakdown -->
{{-- Amount Breakdown --}}
<div class="amount-breakdown-compact">
<h6 class="fw-bold mb-3 text-dark">
<i class="fas fa-calculator me-2"></i>Amount Breakdown
</h6>
<div class="breakdown-row">
<span class="breakdown-label">Total Amount (Before Tax):</span>
<span class="breakdown-value">{{ number_format($invoice->final_amount, 2) }}</span>
<span class="breakdown-label">Total Amount Before Tax</span>
<span class="breakdown-value" id="baseAmount">
{{ number_format($invoice->final_amount, 2) }}
</span>
</div>
<div class="breakdown-row">
<span class="breakdown-label">Tax Type:</span>
<span class="breakdown-label">Tax Type</span>
<span class="breakdown-value text-primary">
@if($invoice->tax_type === 'gst')
GST (CGST + SGST)
@@ -490,7 +489,7 @@ body {
</div>
<div class="breakdown-row">
<span class="breakdown-label">Tax Percentage:</span>
<span class="breakdown-label">Tax Percentage</span>
<span class="breakdown-value text-primary">
@if($invoice->tax_type === 'gst')
{{ $invoice->cgst_percent + $invoice->sgst_percent }}%
@@ -501,114 +500,137 @@ body {
</div>
<div class="breakdown-row">
<span class="breakdown-label">GST Amount:</span>
<span class="breakdown-value text-warning">{{ number_format($invoice->gst_amount, 2) }}</span>
<span class="breakdown-label">GST Amount</span>
<span class="breakdown-value text-warning" id="gstAmount">
{{ number_format($invoice->gst_amount, 2) }}
</span>
</div>
<div class="breakdown-row" style="border-top: 2px solid #e2e8f0; padding-top: 0.75rem;">
<span class="breakdown-label fw-bold">Total Invoice Amount (Including GST):</span>
<span class="breakdown-value fw-bold text-dark">{{ number_format($invoice->final_amount_with_gst, 2) }}</span>
<span class="breakdown-label fw-bold">Total Invoice Amount Including GST</span>
<span class="breakdown-value fw-bold text-dark" id="totalInvoiceWithGst">
{{ number_format($invoice->final_amount_with_gst, 2) }}
</span>
</div>
<div class="breakdown-row">
<span class="breakdown-label text-success">Total Paid:</span>
<span class="breakdown-value fw-bold text-success" id="paidAmount">{{ number_format($totalPaid, 2) }}</span>
<span class="breakdown-label text-success">Total Paid</span>
<span class="breakdown-value fw-bold text-success" id="paidAmount">
{{ number_format($totalPaid, 2) }}
</span>
</div>
<div class="breakdown-row" style="border-bottom: none;">
<span class="breakdown-label text-danger">Remaining:</span>
<span class="breakdown-value fw-bold text-danger" id="remainingAmount">{{ number_format($remaining, 2) }}</span>
<span class="breakdown-label text-danger">Remaining</span>
<span class="breakdown-value fw-bold text-danger" id="remainingAmount">
{{ number_format($remaining, 2) }}
</span>
</div>
</div>
<!-- Installment Summary -->
{{-- Installment Summary --}}
<div class="summary-grid-compact">
<div class="summary-card-compact total">
<div class="summary-value-compact text-success">{{ number_format($invoice->final_amount_with_gst, 2) }}</div>
<div class="summary-value-compact text-success" id="totalInvoiceWithGstCard">
{{ number_format($invoice->final_amount_with_gst, 2) }}
</div>
<div class="summary-label-compact">Total Amount</div>
</div>
<div class="summary-card-compact paid">
<div class="summary-value-compact text-primary">{{ number_format($totalPaid, 2) }}</div>
<div class="summary-value-compact text-primary">
{{ number_format($totalPaid, 2) }}
</div>
<div class="summary-label-compact">Total Paid</div>
</div>
<div class="summary-card-compact remaining">
<div class="summary-value-compact text-warning">{{ number_format($remaining, 2) }}</div>
<div class="summary-value-compact text-warning">
{{ number_format($remaining, 2) }}
</div>
<div class="summary-label-compact">Remaining</div>
</div>
</div>
<!-- Installment Management -->
{{-- Installment Management --}}
<div class="glass-card">
<div class="card-header-compact d-flex justify-content-between align-items-center">
<h4>
<h4 class="mb-0">
<i class="fas fa-credit-card me-2"></i>Installment Payments
</h4>
@can('invoice.add_installment')
@if($remaining > 0)
<button id="toggleInstallmentForm" class="btn-primary-compact btn-compact">
<i class="fas fa-plus-circle me-2"></i>Add Installment
</button>
@endif
@endcan
</div>
<div class="card-body-compact">
<!-- Installment Form -->
{{-- Installment Form --}}
<div id="installmentForm" class="d-none mb-3">
<div class="glass-card" style="background: rgba(248, 250, 252, 0.8);">
<div class="card-header-compact" style="background: var(--success-gradient);">
<h4>
<h4 class="mb-0">
<i class="fas fa-plus-circle me-2"></i>Add New Installment
</h4>
</div>
<div class="card-body-compact">
<form id="installmentSubmitForm">
@csrf
<div class="form-grid-compact">
<div class="form-group-compact">
<label class="form-label-compact">
<i class="fas fa-calendar-day"></i>
Installment Date
<i class="fas fa-calendar-day"></i> Installment Date
</label>
<input type="date" name="installment_date" class="form-control-compact" required>
<input type="date"
name="installment_date"
class="form-control-compact"
required>
</div>
<div class="form-group-compact">
<label class="form-label-compact">
<i class="fas fa-credit-card"></i>
Payment Method
<i class="fas fa-credit-card"></i> Payment Method
</label>
<select name="payment_method" class="form-select-compact" required>
<option value="cash">💵 Cash</option>
<option value="bank">🏦 Bank Transfer</option>
<option value="upi">📱 UPI</option>
<option value="cheque">🧾 Cheque</option>
<select name="payment_method"
class="form-select-compact"
required>
<option value="cash">Cash</option>
<option value="bank">Bank Transfer</option>
<option value="upi">UPI</option>
<option value="cheque">Cheque</option>
</select>
</div>
<div class="form-group-compact">
<label class="form-label-compact">
<i class="fas fa-hashtag"></i>
Reference No
<i class="fas fa-hashtag"></i> Reference No
</label>
<input type="text" name="reference_no" class="form-control-compact"
<input type="text"
name="reference_no"
class="form-control-compact"
placeholder="Enter reference number">
</div>
<div class="form-group-compact">
<label class="form-label-compact">
<i class="fas fa-money-bill-wave"></i>
Amount ()
<i class="fas fa-money-bill-wave"></i> Amount
</label>
<input type="number" name="amount" id="installmentAmount"
class="form-control-compact" step="0.01" min="1"
max="{{ $remaining }}" required
<input type="number"
name="amount"
id="installmentAmount"
class="form-control-compact"
step="0.01"
min="1"
max="{{ $remaining }}"
required
placeholder="Enter installment amount">
</div>
</div>
<div class="text-end mt-2">
<button type="submit" class="btn-success-compact btn-compact" id="installmentSubmitBtn">
<button type="submit"
class="btn-success-compact btn-compact"
id="installmentSubmitBtn">
<i class="fas fa-paper-plane me-2"></i>Submit Installment
</button>
</div>
@@ -617,10 +639,16 @@ body {
</div>
</div>
<!-- Installment History -->
{{-- Installment History --}}
<h6 class="fw-bold mb-2 text-dark">
<i class="fas fa-history me-2"></i>Installment History
</h6>
<div id="noInstallmentsMsg"
class="{{ $invoice->installments->count() ? 'd-none' : '' }} text-center text-muted fw-bold py-4">
No installments found. Click Add Installment to create one.
</div>
<div class="table-responsive">
<table class="table-compact">
<thead>
@@ -640,7 +668,7 @@ body {
<td>{{ $i->installment_date }}</td>
<td>
<span class="badge-compact bg-primary bg-opacity-10 text-primary">
{{ ucfirst($i->payment_method) }}
{{ strtoupper($i->payment_method) }}
</span>
</td>
<td>
@@ -650,7 +678,9 @@ body {
<span class="text-muted">-</span>
@endif
</td>
<td class="fw-bold text-success">{{ number_format($i->amount, 2) }}</td>
<td class="fw-bold text-success">
{{ number_format($i->amount, 2) }}
</td>
<td>
<button class="btn-danger-compact btn-compact btn-sm deleteInstallment">
<i class="fas fa-trash me-1"></i>Delete
@@ -665,40 +695,29 @@ body {
</div>
</div>
<!-- Add this just above the table -->
<div id="noInstallmentsMsg" class="d-none text-center text-muted fw-bold py-4">
No installments found. Click "Add Installment" to create one.
</div>
<table ...>
<tbody id="installmentTable">
@foreach($invoice->installments as $i)
...
@endforeach
</tbody>
</table>
<script>
document.addEventListener("DOMContentLoaded", function () {
// Toggle Installment Form
const toggleBtn = document.getElementById("toggleInstallmentForm");
const formBox = document.getElementById("installmentForm");
if (toggleBtn) {
toggleBtn.addEventListener("click", () => {
if (toggleBtn && formBox) {
toggleBtn.addEventListener("click", function (e) {
e.preventDefault();
formBox.classList.toggle("d-none");
});
}
// Add Installment
const submitForm = document.getElementById("installmentSubmitForm");
const submitBtn = document.getElementById("installmentSubmitBtn");
const formatINR = amt =>
"" + Number(amt).toLocaleString("en-IN", {
const formatINR = (amt) => {
return Number(amt).toLocaleString("en-IN", {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
};
if (submitForm && submitBtn) {
submitForm.addEventListener("submit", function (e) {
e.preventDefault();
@@ -708,8 +727,8 @@ document.addEventListener("DOMContentLoaded", function () {
fetch("{{ route('admin.invoice.installment.store', $invoice->id) }}", {
method: "POST",
headers: {
"X-CSRF-TOKEN": submitForm.querySelector("input[name=_token]").value,
"Accept": "application/json"
"X-CSRF-TOKEN": submitForm.querySelector('input[name="_token"]').value,
"Accept": "application/json",
},
body: new FormData(submitForm)
})
@@ -723,9 +742,11 @@ document.addEventListener("DOMContentLoaded", function () {
return;
}
const table = document.querySelector("#installmentTable");
const table = document.getElementById("installmentTable");
const index = table.rows.length + 1;
document.getElementById("noInstallmentsMsg")?.classList.add("d-none");
table.insertAdjacentHTML("beforeend", `
<tr data-id="${data.installment.id}">
<td class="fw-bold text-muted">${index}</td>
@@ -735,8 +756,12 @@ document.addEventListener("DOMContentLoaded", function () {
${data.installment.payment_method.toUpperCase()}
</span>
</td>
<td>${data.installment.reference_no || '-'}</td>
<td class="fw-bold text-success">${formatINR(data.installment.amount)}</td>
<td>
${data.installment.reference_no ? `<span class="text-muted">${data.installment.reference_no}</span>` : '<span class="text-muted">-</span>'}
</td>
<td class="fw-bold text-success">
${formatINR(data.installment.amount)}
</td>
<td>
<button class="btn-danger-compact btn-compact btn-sm deleteInstallment">
<i class="fas fa-trash me-1"></i>Delete
@@ -745,25 +770,32 @@ document.addEventListener("DOMContentLoaded", function () {
</tr>
`);
// Update all displayed values using GST fields!
if (document.getElementById("paidAmount")) document.getElementById("paidAmount").textContent = formatINR(data.totalPaid);
if (document.getElementById("remainingAmount")) document.getElementById("remainingAmount").textContent = formatINR(data.remaining);
if (document.getElementById("baseAmount")) document.getElementById("baseAmount").textContent = formatINR(data.baseAmount);
if (document.getElementById("gstAmount")) document.getElementById("gstAmount").textContent = formatINR(data.gstAmount);
if (document.getElementById("totalInvoiceWithGst")) document.getElementById("totalInvoiceWithGst").textContent = formatINR(data.finalAmountWithGst);
if (document.getElementById("invoiceStatus")) document.getElementById("invoiceStatus").textContent = data.isCompleted ? "Paid" : "Pending";
if (document.getElementById("paidAmount")) {
document.getElementById("paidAmount").textContent = "" + formatINR(data.totalPaid);
}
if (document.getElementById("remainingAmount")) {
document.getElementById("remainingAmount").textContent = "" + formatINR(data.remaining);
}
if (document.getElementById("baseAmount")) {
document.getElementById("baseAmount").textContent = "" + formatINR(data.baseAmount);
}
if (document.getElementById("gstAmount")) {
document.getElementById("gstAmount").textContent = "" + formatINR(data.gstAmount);
}
if (document.getElementById("totalInvoiceWithGst")) {
document.getElementById("totalInvoiceWithGst").textContent = "" + formatINR(data.finalAmountWithGst);
}
// Update summary cards if used
const paidCard = document.querySelector(".summary-card-compact.paid .summary-value-compact");
if (paidCard) paidCard.textContent = formatINR(data.totalPaid);
if (paidCard) paidCard.textContent = "" + formatINR(data.totalPaid);
const remainingCard = document.querySelector(".summary-card-compact.remaining .summary-value-compact");
if (remainingCard) remainingCard.textContent = formatINR(data.remaining);
if (remainingCard) remainingCard.textContent = "" + formatINR(data.remaining);
submitForm.reset();
// If fully paid, disable/add display logic
if (data.isCompleted) {
toggleBtn?.remove();
if (data.isCompleted && toggleBtn) {
toggleBtn.remove();
formBox.classList.add("d-none");
}
@@ -775,44 +807,61 @@ document.addEventListener("DOMContentLoaded", function () {
alert("Something went wrong. Please try again.");
});
});
}
// Delete Installment
document.addEventListener("click", function (e) {
if (!e.target.classList.contains("deleteInstallment")) return;
if (!e.target.classList.contains("deleteInstallment") &&
!e.target.closest(".deleteInstallment")) {
return;
}
e.preventDefault();
if (!confirm("Are you sure you want to delete this installment?")) return;
if (!confirm("Are you sure you want to delete this installment?")) {
return;
}
const row = e.target.closest("tr");
const btn = e.target.closest(".deleteInstallment");
const row = btn.closest("tr");
const id = row.getAttribute("data-id");
fetch("{{ url('/admin/installment') }}/" + id, {
fetch("{{ url('admin/installment') }}/" + id, {
method: "DELETE",
headers: {
"X-CSRF-TOKEN": "{{ csrf_token() }}",
"Accept": "application/json"
}
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').getAttribute("content"),
"Accept": "application/json",
},
})
.then(res => res.json())
.then(data => {
if (data.status === "success") {
row.style.opacity = "0";
row.style.opacity = 0;
setTimeout(() => row.remove(), 300);
// Update all displayed values using GST fields!
if (document.getElementById("paidAmount")) document.getElementById("paidAmount").textContent = formatINR(data.totalPaid);
if (document.getElementById("remainingAmount")) document.getElementById("remainingAmount").textContent = formatINR(data.remaining);
if (document.getElementById("baseAmount")) document.getElementById("baseAmount").textContent = formatINR(data.baseAmount);
if (document.getElementById("gstAmount")) document.getElementById("gstAmount").textContent = formatINR(data.gstAmount);
if (document.getElementById("totalInvoiceWithGst")) document.getElementById("totalInvoiceWithGst").textContent = formatINR(data.finalAmountWithGst);
if (document.getElementById("invoiceStatus")) document.getElementById("invoiceStatus").textContent = data.remaining === 0 ? "Paid" : "Pending";
if (document.getElementById("paidAmount")) {
document.getElementById("paidAmount").textContent = "" + formatINR(data.totalPaid);
}
if (document.getElementById("remainingAmount")) {
document.getElementById("remainingAmount").textContent = "" + formatINR(data.remaining);
}
if (document.getElementById("baseAmount")) {
document.getElementById("baseAmount").textContent = "" + formatINR(data.baseAmount);
}
if (document.getElementById("gstAmount")) {
document.getElementById("gstAmount").textContent = "" + formatINR(data.gstAmount);
}
if (document.getElementById("totalInvoiceWithGst")) {
document.getElementById("totalInvoiceWithGst").textContent = "" + formatINR(data.finalAmountWithGst);
}
// Update summary cards
const paidCard = document.querySelector(".summary-card-compact.paid .summary-value-compact");
if (paidCard) paidCard.textContent = formatINR(data.totalPaid);
if (paidCard) paidCard.textContent = "" + formatINR(data.totalPaid);
const remainingCard = document.querySelector(".summary-card-compact.remaining .summary-value-compact");
if (remainingCard) remainingCard.textContent = formatINR(data.remaining);
if (remainingCard) remainingCard.textContent = "" + formatINR(data.remaining);
if (data.isZero) {
document.getElementById("noInstallmentsMsg")?.classList.remove("d-none");
}
alert(data.message);
} else {
@@ -825,6 +874,4 @@ document.addEventListener("DOMContentLoaded", function () {
});
});
</script>
@endsection

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;
@@ -220,12 +220,16 @@
font-weight: 500;
}
</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) --}}
@@ -234,12 +238,19 @@
<i class="bi bi-house"></i> Dashboard
</a>
@endcan
<!--
{{-- Shipments --}}
@can('shipment.view')
<a href="{{ route('admin.shipments') }}" class="{{ request()->routeIs('admin.shipments') ? 'active' : '' }}">
<i class="bi bi-truck"></i> Shipments
</a>
@endcan -->
{{-- 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 --}}
@@ -263,10 +274,12 @@
</a>
@endcan
{{-- Chat Support (NO PERMISSION REQUIRED) --}}
{{-- 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')
@@ -284,15 +297,17 @@
{{-- Profile Update Requests --}}
@can('request.update_profile')
<a href="{{ route('admin.profile.requests') }}">
<a href="{{ route('admin.profile.requests') }}" class="{{ request()->routeIs('admin.profile.requests') ? 'active' : '' }}">
<i class="bi bi-person-lines-fill"></i> Profile Update Requests
</a>
@endcan
{{-- Staff (NO PERMISSION REQUIRED) --}}
{{-- 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')
@@ -325,40 +340,49 @@
<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')
<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);
}

View File

@@ -5,7 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Professional Invoice</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
:root {
--primary: #2c3e50;
@@ -21,7 +23,7 @@
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.5;
@@ -30,14 +32,14 @@
.invoice-container {
max-width: 1200px;
margin: 2rem auto;
background: white;
background: #ffffff;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
overflow: hidden;
}
.invoice-header {
background: white;
background: #ffffff;
padding: 1.5rem 2rem;
border-bottom: 1px solid #e9ecef;
}
@@ -56,11 +58,11 @@
}
.id-container {
margin-bottom: 1rem; /* Reduced from 1.5rem */
margin-bottom: 1rem;
}
.id-box {
background: white;
background: #ffffff;
border-radius: var(--border-radius);
padding: 1rem;
border: 1px solid #e9ecef;
@@ -82,10 +84,6 @@
border-left: 4px solid var(--success);
}
.id-box-accent {
border-left: 4px solid var(--warning);
}
.id-icon {
width: 36px;
height: 36px;
@@ -93,7 +91,7 @@
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.5rem; /* Reduced from 0.75rem */
margin-bottom: 0.5rem;
font-size: 1rem;
}
@@ -107,11 +105,6 @@
color: var(--success);
}
.id-icon-accent {
background: rgba(243, 156, 18, 0.1);
color: var(--warning);
}
.id-label {
font-size: 0.75rem;
color: #6c757d;
@@ -129,10 +122,10 @@
}
.date-container {
background: white;
background: #ffffff;
border-radius: var(--border-radius);
padding: 1rem; /* Reduced from 1.25rem */
margin-bottom: 1rem; /* Reduced from 1.5rem */
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid #e9ecef;
box-shadow: var(--box-shadow);
}
@@ -152,9 +145,9 @@
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 0.5rem; /* Reduced from 0.75rem */
margin: 0 auto 0.5rem;
background: var(--secondary);
color: white;
color: #ffffff;
font-size: 1rem;
}
@@ -172,7 +165,7 @@
font-weight: 700;
color: var(--primary);
padding: 0.5rem;
background: white;
background: #ffffff;
border-radius: 4px;
border: 1px solid #e9ecef;
}
@@ -195,7 +188,7 @@
.card {
border: 1px solid #e9ecef;
border-radius: var(--border-radius);
margin-bottom: 1rem; /* Reduced from 1.5rem */
margin-bottom: 1rem;
box-shadow: var(--box-shadow);
}
@@ -235,18 +228,7 @@
.summary-header {
background: var(--primary);
color: white;
}
.amount-row {
border-bottom: 1px solid #e9ecef;
padding: 0.75rem 0;
}
.total-row {
border-top: 2px solid #dee2e6;
font-size: 1.1rem;
font-weight: 700;
color: #ffffff;
}
.text-primary {
@@ -266,17 +248,16 @@
padding: 0.35rem 0.65rem;
}
/* COMPACT HEADER STYLES */
.compact-header {
margin-bottom: 0.75rem; /* Reduced from default */
margin-bottom: 0.75rem;
}
.compact-header .invoice-title {
margin-bottom: 0.25rem; /* Reduced gap */
margin-bottom: 0.25rem;
}
.compact-header .status-badge {
margin-top: 0.25rem; /* Reduced gap */
margin-top: 0.25rem;
}
@media (max-width: 768px) {
@@ -301,13 +282,11 @@
<body>
<div class="invoice-container">
<div class="p-4">
<!-- ============================
INVOICE HEADER - COMPACT
============================ -->
@php
$showActions = $showActions ?? true;
@endphp
{{-- HEADER --}}
<div class="compact-header">
<div class="row align-items-center">
<div class="col-md-6">
@@ -316,7 +295,6 @@
</h2>
<h4 class="fw-bold text-dark mb-0">{{ $invoice->invoice_number }}</h4>
</div>
<div class="col-md-6 text-end">
<span class="status-badge
@if($invoice->status == 'paid') bg-success
@@ -334,11 +312,11 @@
</div>
</div>
<!-- Three ID Boxes in One Row -->
{{-- ID BOXES: INVOICE + CONTAINER --}}
<div class="id-container">
<div class="row">
<!-- Invoice ID Box -->
<div class="col-md-4 mb-3">
{{-- Invoice ID Box --}}
<div class="col-md-6 mb-3">
<div class="id-box id-box-primary">
<div class="id-icon id-icon-primary">
<i class="fas fa-receipt"></i>
@@ -348,18 +326,18 @@
</div>
</div>
<!-- Order ID Box -->
<div class="col-md-4 mb-3">
{{-- Container ID Box --}}
<div class="col-md-6 mb-3">
<div class="id-box id-box-secondary">
<div class="id-icon id-icon-secondary">
<i class="fas fa-shopping-cart"></i>
<i class="fas fa-box"></i>
</div>
<div class="id-label">Order ID</div>
<div class="id-label">Container ID</div>
<div class="id-value">
@if($invoice->order && $invoice->order->order_id)
{{ $invoice->order->order_id }}
@elseif($invoice->order_id)
{{ $invoice->order_id }}
@if($invoice->container && $invoice->container->container_number)
{{ $invoice->container->container_number }}
@elseif($invoice->container_id)
{{ $invoice->container_id }}
@else
N/A
@endif
@@ -367,35 +345,8 @@
</div>
</div>
<!-- Shipment ID Box -->
<div class="col-md-4 mb-3">
<div class="id-box id-box-accent">
<div class="id-icon id-icon-accent">
<i class="fas fa-shipping-fast"></i>
</div>
<div class="id-label">Shipment ID</div>
<div class="id-value">
@php
$shipmentId = 'N/A';
// Try multiple ways to get shipment ID
if($invoice->shipment && $invoice->shipment->shipment_id) {
$shipmentId = $invoice->shipment->shipment_id;
} elseif($invoice->shipment_id) {
$shipmentId = $invoice->shipment_id;
} elseif(isset($shipment) && $shipment && $shipment->shipment_id) {
$shipmentId = $shipment->shipment_id;
}
@endphp
{{ $shipmentId }}
</div>
</div>
</div>
</div>
</div>
<!-- ============================
DATES SECTION
============================ -->
{{-- DATES --}}
<div class="date-container">
<div class="row align-items-center">
<div class="col-md-5">
@@ -404,7 +355,9 @@
<i class="fas fa-calendar-alt"></i>
</div>
<div class="date-label">INVOICE DATE</div>
<div class="date-value">{{ \Carbon\Carbon::parse($invoice->invoice_date)->format('M d, Y') }}</div>
<div class="date-value">
{{ \Carbon\Carbon::parse($invoice->invoice_date)->format('M d, Y') }}
</div>
</div>
</div>
<div class="col-md-2">
@@ -426,9 +379,7 @@
</div>
</div>
<!-- ============================
CUSTOMER DETAILS
============================ -->
{{-- CUSTOMER DETAILS --}}
<div class="card">
<div class="card-header">
<h6 class="mb-0 fw-bold">
@@ -464,9 +415,11 @@
</div>
</div>
<!-- ============================
INVOICE ITEMS
============================ -->
{{-- INVOICE ITEMS (EDITABLE WHEN EMBEDDED) --}}
@php
$isEmbedded = isset($embedded) && $embedded;
@endphp
<div class="card">
<div class="card-header">
<h6 class="mb-0 fw-bold">
@@ -474,6 +427,13 @@
</h6>
</div>
<div class="card-body p-0">
@if($isEmbedded)
<form action="{{ route('admin.invoices.items.update', $invoice->id) }}" method="POST">
@csrf
@method('PUT')
@endif
<div class="table-responsive">
<table class="table table-bordered table-hover align-middle mb-0">
<thead class="table-light">
@@ -502,8 +462,35 @@
<td class="text-center">{{ $item->qty }}</td>
<td class="text-center fw-bold">{{ $item->ttl_qty }}</td>
<td class="text-center">{{ $item->unit }}</td>
<td class="text-center text-success fw-bold">{{ number_format($item->price,2) }}</td>
<td class="text-center text-primary fw-bold">{{ number_format($item->ttl_amount,2) }}</td>
@if($isEmbedded)
{{-- EDIT MODE (invoice_edit page) --}}
<td class="text-center" style="width:120px;">
<input type="number"
step="0.01"
min="0"
name="items[{{ $item->id }}][price]"
value="{{ old('items.' . $item->id . '.price', $item->price) }}"
class="form-control form-control-sm text-end">
</td>
<td class="text-center" style="width:140px;">
<input type="number"
step="0.01"
min="0"
name="items[{{ $item->id }}][ttl_amount]"
value="{{ old('items.' . $item->id . '.ttl_amount', $item->ttl_amount) }}"
class="form-control form-control-sm text-end">
</td>
@else
{{-- NORMAL POPUP (READ-ONLY) --}}
<td class="text-center text-success fw-bold">
{{ number_format($item->price, 2) }}
</td>
<td class="text-center text-primary fw-bold">
{{ number_format($item->ttl_amount, 2) }}
</td>
@endif
<td class="text-center">{{ $item->cbm }}</td>
<td class="text-center">{{ $item->ttl_cbm }}</td>
<td class="text-center">{{ $item->kg }}</td>
@@ -513,15 +500,30 @@
</td>
</tr>
@endforeach
@if($invoice->items->isEmpty())
<tr>
<td colspan="13" class="text-center text-muted fw-bold py-3">
No invoice items found.
</td>
</tr>
@endif
</tbody>
</table>
</div>
@if($isEmbedded)
<div class="text-end p-3">
<button type="submit" class="btn btn-primary btn-sm">
<i class="fas fa-save me-1"></i> Update Items & Recalculate
</button>
</div>
</form>
@endif
</div>
</div>
<!-- ============================
FINAL SUMMARY
============================ -->
{{-- FINAL SUMMARY --}}
<div class="row">
<div class="col-md-6 offset-md-6">
<div class="card summary-card">
@@ -533,61 +535,78 @@
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
<span class="fw-semibold">Amount:</span>
<span class="fw-bold text-dark">{{ number_format($invoice->final_amount,2) }}</span>
<span class="fw-bold text-dark">
{{ number_format($invoice->final_amount, 2) }}
</span>
</div>
@if($invoice->tax_type === 'gst')
{{-- CGST --}}
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
<span class="fw-semibold">CGST ({{ $invoice->cgst_percent ?? ($invoice->gst_percent/2) }}%):</span>
<span class="fw-bold text-danger">{{ number_format($invoice->gst_amount/2, 2) }}</span>
<span class="fw-semibold">
CGST ({{ $invoice->cgst_percent ?? ($invoice->gst_percent / 2) }}%):
</span>
<span class="fw-bold text-danger">
{{ number_format($invoice->gst_amount / 2, 2) }}
</span>
</div>
{{-- SGST --}}
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
<span class="fw-semibold">SGST ({{ $invoice->sgst_percent ?? ($invoice->gst_percent/2) }}%):</span>
<span class="fw-bold text-danger">{{ number_format($invoice->gst_amount/2, 2) }}</span>
<span class="fw-semibold">
SGST ({{ $invoice->sgst_percent ?? ($invoice->gst_percent / 2) }}%):
</span>
<span class="fw-bold text-danger">
{{ number_format($invoice->gst_amount / 2, 2) }}
</span>
</div>
@elseif($invoice->tax_type === 'igst')
{{-- IGST --}}
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
<span class="fw-semibold">IGST ({{ $invoice->igst_percent ?? $invoice->gst_percent }}%):</span>
<span class="fw-bold text-danger">{{ number_format($invoice->gst_amount, 2) }}</span>
<span class="fw-semibold">
IGST ({{ $invoice->igst_percent ?? $invoice->gst_percent }}%):
</span>
<span class="fw-bold text-danger">
{{ number_format($invoice->gst_amount, 2) }}
</span>
</div>
@else
{{-- Default GST --}}
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
<span class="fw-semibold">GST ({{ $invoice->gst_percent }}%):</span>
<span class="fw-bold text-danger">{{ number_format($invoice->gst_amount, 2) }}</span>
<span class="fw-semibold">
GST ({{ $invoice->gst_percent }}%):
</span>
<span class="fw-bold text-danger">
{{ number_format($invoice->gst_amount, 2) }}
</span>
</div>
@endif
<div class="d-flex justify-content-between align-items-center pt-1">
<span class="fw-bold text-dark">Total Payable:</span>
<span class="fw-bold text-success">{{ number_format($invoice->final_amount_with_gst,2) }}</span>
<span class="fw-bold text-success">
{{ number_format($invoice->final_amount_with_gst, 2) }}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- ============================
FOOTER DOWNLOAD & SHARE
============================ -->
{{-- FOOTER ACTIONS --}}
<div class="mt-4 pt-3 border-top text-center">
@if($invoice->pdf_path)
<a href="{{ asset($invoice->pdf_path) }}" class="btn btn-primary me-2" download>
@if($invoice->pdf_path && $showActions)
<a href="{{ asset($invoice->pdf_path) }}"
class="btn btn-primary me-2" download>
<i class="fas fa-download me-1"></i> Download PDF
</a>
<button class="btn btn-success" onclick="shareInvoice()">
<i class="fas fa-share me-1"></i> Share
</button>
@endif
</div>
<!-- Footer Message -->
{{-- FOOTER MESSAGE --}}
<div class="mt-4 pt-3 border-top text-center text-muted">
<p class="mb-1">Thank you for your business!</p>
<p class="mb-0">For any inquiries, contact us at support@Kent Logistic</p>
@@ -596,10 +615,6 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
<!-- ============================
SHARE SCRIPT
============================ -->
<script>
function shareInvoice() {
const shareData = {

View File

@@ -1,11 +1,22 @@
<?php
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\RequestController;
use App\Http\Controllers\UserAuthController;
use App\Http\Controllers\MarkListController;
use App\Http\Controllers\User\UserOrderController;
use App\Http\Controllers\User\UserProfileController;
use App\Http\Controllers\User\ChatController;
Broadcast::routes(['middleware' => ['auth:api']]);
//user send request
@@ -46,4 +57,22 @@ Route::middleware(['auth:api'])->group(function () {
Route::post('/user/profile-update-request', [UserProfileController::class, 'updateProfileRequest']);
// Route::post('/user/profile/update', [UserProfileController::class, 'updateProfile']);
// ===========================
// CHAT SUPPORT ROUTES
// ===========================
Route::get('/user/chat/start', [ChatController::class, 'startChat']);
Route::get('/user/chat/messages/{ticketId}', [ChatController::class, 'getMessages']);
Route::post('/user/chat/send/{ticketId}', [ChatController::class, 'sendMessage']);
});
Route::post('/broadcasting/auth', function (Request $request) {
if (!auth()->check()) {
return response()->json(['message' => 'Unauthenticated'], 401);
}
return Broadcast::auth($request);
})->middleware('auth:api');

21
routes/broadcasting.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Facades\Route;
Broadcast::routes(['middleware' => ['web']]);
// Force admin guard authentication
Route::post('/broadcasting/auth', function () {
if (auth('admin')->check()) {
return Broadcast::auth(request());
}
// Fallback check for normal users
if (auth('web')->check()) {
return Broadcast::auth(request());
}
return response()->json(['message' => 'Unauthenticated'], 403);
})->middleware(['web']);

42
routes/channels.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
use Illuminate\Support\Facades\Broadcast;
use App\Models\SupportTicket;
/*
|--------------------------------------------------------------------------
| Broadcast Channels
|--------------------------------------------------------------------------
*/
Broadcast::channel('ticket.{ticketId}', function ($user, $ticketId) {
\Log::info('🔐 Broadcasting Auth Check', [
'ticketId' => $ticketId,
'user_id' => $user->id ?? 'NULL',
'user_table' => method_exists($user, 'getTable') ? $user->getTable() : 'unknown',
'user_class' => get_class($user)
]);
$ticket = SupportTicket::find($ticketId);
if (!$ticket) {
\Log::warning('❌ Ticket not found', ['ticketId' => $ticketId]);
return false;
}
// ✅ Admin/Staff Check (Session Auth)
if (get_class($user) === 'App\Models\Admin') {
\Log::info('✅ Admin authorized for ticket', ['admin_id' => $user->id]);
return true;
}
// ✅ User Check (JWT Auth - must own ticket)
if (get_class($user) === 'App\Models\User' && $ticket->user_id === $user->id) {
\Log::info('✅ User authorized for own ticket', ['user_id' => $user->id]);
return true;
}
\Log::warning('❌ Authorization failed');
return false;
});

View File

@@ -0,0 +1,54 @@
@extends('admin.layouts.app')
@section('page-title', 'Containers')
@section('content')
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Container List</h5>
<a href="{{ route('containers.create') }}" class="btn btn-primary btn-sm">
Add Container
</a>
</div>
<div class="card-body">
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
@if($containers->isEmpty())
<p class="mb-0">No containers found.</p>
@else
<table class="table table-bordered table-striped align-middle">
<thead class="table-light">
<tr>
<th>#</th>
<th>Customer Name</th>
<th>Container Number</th>
<th>Date</th>
<th>Excel File</th>
</tr>
</thead>
<tbody>
@foreach($containers as $container)
<tr>
<td>{{ $loop->iteration }}</td>
<td>{{ $container->customer_name }}</td>
<td>{{ $container->container_number }}</td>
<td>{{ $container->container_date?->format('d-m-Y') }}</td>
<td>
@if($container->excel_file)
<a href="{{ Storage::url($container->excel_file) }}" target="_blank">
Download
</a>
@else
-
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
@endif
</div>
</div>
@endsection

View File

@@ -11,6 +11,10 @@ use App\Http\Controllers\Admin\AdminCustomerController;
use App\Http\Controllers\Admin\AdminAccountController;
use App\Http\Controllers\Admin\AdminReportController;
use App\Http\Controllers\Admin\AdminStaffController;
use App\Http\Controllers\Admin\AdminChatController;
use Illuminate\Support\Facades\Broadcast;
use App\Http\Controllers\ContainerController;
// ---------------------------
// Public Front Page
@@ -19,6 +23,41 @@ Route::get('/', function () {
return view('welcome');
});
// ADD THIS BEFORE YOUR ADMIN ROUTES (Around Line 20-25)
// ==========================================
// BROADCASTING AUTH (FOR ADMIN SESSION)
// ==========================================
Broadcast::routes(['middleware' => ['web']]);
// Custom broadcasting auth for admin guard
Route::post('/broadcasting/auth', function (\Illuminate\Http\Request $request) {
\Log::info('🎯 Broadcasting Auth Request', [
'channel' => $request->input('channel_name'),
'admin_check' => auth('admin')->check(),
'web_check' => auth('web')->check(),
]);
// ✅ Admin Guard (Session)
if (auth('admin')->check()) {
\Log::info('✅ Admin authenticated', ['id' => auth('admin')->id()]);
return Broadcast::auth($request);
}
// ✅ Web Guard (Fallback for Users)
if (auth('web')->check()) {
\Log::info('✅ Web user authenticated', ['id' => auth('web')->id()]);
return Broadcast::auth($request);
}
\Log::warning('❌ No authentication found');
return response()->json(['message' => 'Unauthenticated'], 403);
})->middleware('web');
// ---------------------------
// ADMIN LOGIN ROUTES
// ---------------------------
@@ -27,9 +66,15 @@ Route::prefix('admin')->group(function () {
Route::get('/login', [AdminAuthController::class, 'showLoginForm'])->name('admin.login');
Route::post('/login', [AdminAuthController::class, 'login'])->name('admin.login.submit');
Route::post('/logout', [AdminAuthController::class, 'logout'])->name('admin.logout');
});
//Broadcast::routes([
//'middleware' => ['web', 'auth:admin'],
//]);
Broadcast::routes(['middleware' => ['web']]);
// ==========================================
// PROTECTED ADMIN ROUTES (session protected)
// ==========================================
@@ -56,6 +101,33 @@ Route::prefix('admin')
->name('admin.profile');
//---------------------------
// CONTAINER ROUTES
//---------------------------
Route::get('/containers', [ContainerController::class, 'index'])
->name('containers.index');
// Container create form
Route::get('/containers/create', [ContainerController::class, 'create'])
->name('containers.create');
// Container store
Route::post('/containers', [ContainerController::class, 'store'])
->name('containers.store');
// Container delete
Route::resource('containers', ContainerController::class);
//status update
Route::post('containers/{container}/status', [ContainerController::class, 'updateStatus'])
->name('containers.update-status');
// ---------------------------
// USER REQUESTS
// ---------------------------
@@ -199,6 +271,11 @@ Route::prefix('admin')
->name('admin.invoice.installment.delete');
Route::put('admin/invoices/{invoice}/items', [AdminInvoiceController::class, 'updateItems'])
->name('admin.invoices.items.update');
//Add New Invoice
Route::get('/admin/invoices/create', [InvoiceController::class, 'create'])->name('admin.invoices.create');
@@ -220,6 +297,19 @@ Route::prefix('admin')
Route::post('/customers/{id}/status', [AdminCustomerController::class, 'toggleStatus'])
->name('admin.customers.status');
// Chat list page
Route::get('/chat-support', [AdminChatController::class, 'index'])
->name('admin.chat_support');
// Chat window (open specific user's chat)
Route::get('/chat-support/{ticketId}', [AdminChatController::class, 'openChat'])
->name('admin.chat.open');
// Admin sending message
Route::post('/chat-support/{ticketId}/send', [AdminChatController::class, 'sendMessage'])
->name('admin.chat.send');
});
// ==========================================
@@ -302,3 +392,22 @@ Route::prefix('admin')
// staff resource
Route::resource('staff', AdminStaffController::class);
});
// routes/web.php Line 57 (admin routes PREFIX ANTA)
Route::prefix('admin')->middleware('auth:admin')->group(function () {
// ... your routes
});
Route::post('/admin/broadcasting/auth', function () {
return Broadcast::auth(request());
})->middleware('auth:admin');
// Container row update route
Route::post('/containers/{container}/update-rows', [ContainerController::class, 'updateRows'])
->name('containers.rows.update');

View File

@@ -5,7 +5,11 @@ import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
input: [
"resources/css/app.css",
"resources/js/app.js",
// ❌ Remove echo.js from here - it's imported in app.js
],
refresh: true,
}),
tailwindcss(),