Compare commits
2 Commits
8b6d3d5fad
...
33571a5fd7
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
33571a5fd7 | ||
|
|
94e211f87e |
12
.env.example
12
.env.example
@@ -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
|
||||
|
||||
@@ -4,10 +4,10 @@ namespace App\Events;
|
||||
|
||||
use App\Models\ChatMessage;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class NewChatMessage implements ShouldBroadcastNow
|
||||
class NewChatMessage implements ShouldBroadcast
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
@@ -18,22 +18,29 @@ class NewChatMessage implements ShouldBroadcastNow
|
||||
*/
|
||||
public function __construct(ChatMessage $message)
|
||||
{
|
||||
// Also load sender polymorphic relationship
|
||||
$message->load('sender');
|
||||
// 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(),
|
||||
];
|
||||
|
||||
$this->message = $message;
|
||||
// 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),
|
||||
new PrivateChannel('admin.chat') // 👈 ADD THIS
|
||||
];
|
||||
|
||||
return new PrivateChannel('ticket.' . $this->message->ticket_id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,40 +48,25 @@ class NewChatMessage implements ShouldBroadcastNow
|
||||
*/
|
||||
public function broadcastWith()
|
||||
{
|
||||
\Log::info('APP_URL USED IN EVENT', [
|
||||
'url' => config('app.url'),
|
||||
]);
|
||||
|
||||
\Log::info("DEBUG: NewChatMessage broadcasting on channel ticket.".$this->message->ticket_id);
|
||||
|
||||
\Log::info("EVENT BROADCAST FIRED", [
|
||||
'channel' => 'ticket.'.$this->message->ticket_id,
|
||||
'sender_type' => $this->message->sender_type,
|
||||
'sender_id' => $this->message->sender_id,
|
||||
]);
|
||||
|
||||
|
||||
return [
|
||||
'id' => $this->message->id,
|
||||
'ticket_id' => $this->message->ticket_id,
|
||||
'sender_id' => $this->message->sender_id,
|
||||
'sender_type' => $this->message->sender_type,
|
||||
'message' => $this->message->message,
|
||||
'client_id' => $this->message->client_id,
|
||||
|
||||
// ✅ relative path only
|
||||
'file_path' => $this->message->file_path ?? null,
|
||||
'file_type' => $this->message->file_type ?? null,
|
||||
|
||||
'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(),
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,10 +84,4 @@ class NewChatMessage implements ShouldBroadcastNow
|
||||
// Admin model has ->name
|
||||
return $sender->name ?? "Admin";
|
||||
}
|
||||
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'NewChatMessage';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,14 +3,35 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
<<<<<<< HEAD
|
||||
use App\Models\SupportTicket;
|
||||
use App\Models\ChatMessage;
|
||||
use App\Events\NewChatMessage;
|
||||
use Illuminate\Http\Request;
|
||||
=======
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\SupportTicket;
|
||||
use App\Models\ChatMessage;
|
||||
use App\Events\NewChatMessage;
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
|
||||
class AdminChatController extends Controller
|
||||
{
|
||||
/**
|
||||
<<<<<<< HEAD
|
||||
* Page 1: List all customer chat tickets
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$tickets = SupportTicket::with(['user', 'messages' => function($query) {
|
||||
$query->latest()->limit(1);
|
||||
}])
|
||||
->orderBy('updated_at', 'desc')
|
||||
->get();
|
||||
|
||||
return view('admin.chat_support', compact('tickets'));
|
||||
}
|
||||
=======
|
||||
* Page 1: List all active user chats
|
||||
*/
|
||||
public function index()
|
||||
@@ -28,11 +49,26 @@ class AdminChatController extends Controller
|
||||
return view('admin.chat_support', compact('tickets'));
|
||||
}
|
||||
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
|
||||
/**
|
||||
* Page 2: Open chat window for a specific user
|
||||
*/
|
||||
public function openChat($ticketId)
|
||||
<<<<<<< HEAD
|
||||
{
|
||||
$ticket = SupportTicket::with('user')->findOrFail($ticketId);
|
||||
$messages = ChatMessage::where('ticket_id', $ticketId)
|
||||
->orderBy('created_at', 'asc')
|
||||
->with('sender')
|
||||
->get();
|
||||
|
||||
return view('admin.chat_window', compact('ticket', 'messages'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin sends a message to the user (FIXED - LIVE CHAT)
|
||||
=======
|
||||
{
|
||||
$ticket = SupportTicket::with('user')->findOrFail($ticketId);
|
||||
|
||||
@@ -53,6 +89,7 @@ class AdminChatController extends Controller
|
||||
|
||||
/**
|
||||
* Admin sends a message to the user
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
*/
|
||||
public function sendMessage(Request $request, $ticketId)
|
||||
{
|
||||
@@ -69,9 +106,12 @@ class AdminChatController extends Controller
|
||||
'sender_id' => $admin->id,
|
||||
'sender_type' => \App\Models\Admin::class,
|
||||
'message' => $request->message,
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
'read_by_admin' => true,
|
||||
'read_by_user' => false,
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
];
|
||||
|
||||
// File Upload
|
||||
@@ -85,11 +125,20 @@ class AdminChatController extends Controller
|
||||
$message = ChatMessage::create($data);
|
||||
$message->load('sender');
|
||||
|
||||
<<<<<<< HEAD
|
||||
\Log::info("DEBUG: ChatController sendMessage called", [
|
||||
=======
|
||||
\Log::info("DEBUG: ChatController sendMessage called", [
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
'ticket_id' => $ticketId,
|
||||
'payload' => $request->all()
|
||||
]);
|
||||
|
||||
<<<<<<< HEAD
|
||||
// 🔥 LIVE CHAT - Queue bypass (100% working)
|
||||
broadcast(new NewChatMessage($message))->toOthers();
|
||||
|
||||
=======
|
||||
// Broadcast real-time
|
||||
broadcast(new NewChatMessage($message));
|
||||
|
||||
@@ -97,6 +146,7 @@ class AdminChatController extends Controller
|
||||
'ticket_id' => $ticketId,
|
||||
'payload' => $request->all()
|
||||
]);
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message
|
||||
|
||||
@@ -3,13 +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 Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Mpdf\Mpdf;
|
||||
|
||||
class AdminInvoiceController extends Controller
|
||||
{
|
||||
@@ -18,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'));
|
||||
}
|
||||
|
||||
@@ -27,11 +29,8 @@ class AdminInvoiceController extends Controller
|
||||
// -------------------------------------------------------------
|
||||
public function popup($id)
|
||||
{
|
||||
$invoice = Invoice::with(['items', 'order', 'installments'])->findOrFail($id);
|
||||
|
||||
$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'));
|
||||
}
|
||||
@@ -41,8 +40,8 @@ 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;
|
||||
|
||||
// ADD THIS SECTION: Calculate customer's total due across all invoices
|
||||
$customerTotalDue = Invoice::where('customer_id', $invoice->customer_id)
|
||||
@@ -55,17 +54,21 @@ class AdminInvoiceController extends Controller
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 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',
|
||||
@@ -76,31 +79,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'],
|
||||
@@ -111,13 +115,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()
|
||||
@@ -125,13 +144,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/');
|
||||
|
||||
@@ -140,14 +235,25 @@ 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]);
|
||||
}
|
||||
|
||||
@@ -163,7 +269,7 @@ class AdminInvoiceController extends Controller
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INSTALLMENTS (ADD/DELETE)
|
||||
// INSTALLMENTS (ADD)
|
||||
// -------------------------------------------------------------
|
||||
public function storeInstallment(Request $request, $invoice_id)
|
||||
{
|
||||
@@ -175,15 +281,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);
|
||||
}
|
||||
|
||||
@@ -197,7 +301,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']);
|
||||
|
||||
@@ -213,9 +316,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);
|
||||
@@ -226,8 +333,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']);
|
||||
|
||||
$this->generateInvoicePDF($invoice);
|
||||
@@ -241,8 +347,7 @@ class AdminInvoiceController extends Controller
|
||||
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
||||
'baseAmount' => $invoice->final_amount,
|
||||
'remaining' => $remaining,
|
||||
'isZero' => $paidTotal == 0
|
||||
'isZero' => $paidTotal == 0,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -41,9 +41,52 @@ class AdminOrderController extends Controller
|
||||
return view('admin.orders_create', compact('markList'));
|
||||
}
|
||||
|
||||
/* ---------------------------
|
||||
* SHOW / POPUP
|
||||
* ---------------------------*/
|
||||
/**
|
||||
* Store a new order and optionally create initial invoice
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'mark_no' => 'required|string',
|
||||
'origin' => 'nullable|string',
|
||||
'destination' => 'nullable|string',
|
||||
// totals optional when creating without items
|
||||
'ctn' => 'nullable|numeric',
|
||||
'qty' => 'nullable|numeric',
|
||||
'ttl_qty' => 'nullable|numeric',
|
||||
'ttl_amount' => 'nullable|numeric',
|
||||
'cbm' => 'nullable|numeric',
|
||||
'ttl_cbm' => 'nullable|numeric',
|
||||
'kg' => 'nullable|numeric',
|
||||
'ttl_kg' => 'nullable|numeric',
|
||||
]);
|
||||
|
||||
$order = Order::create([
|
||||
'order_id' => $this->generateOrderId(),
|
||||
'mark_no' => $data['mark_no'],
|
||||
'origin' => $data['origin'] ?? null,
|
||||
'destination' => $data['destination'] ?? null,
|
||||
'ctn' => $data['ctn'] ?? 0,
|
||||
'qty' => $data['qty'] ?? 0,
|
||||
'ttl_qty' => $data['ttl_qty'] ?? 0,
|
||||
'ttl_amount' => $data['ttl_amount'] ?? 0,
|
||||
'cbm' => $data['cbm'] ?? 0,
|
||||
'ttl_cbm' => $data['ttl_cbm'] ?? 0,
|
||||
'kg' => $data['kg'] ?? 0,
|
||||
'ttl_kg' => $data['ttl_kg'] ?? 0,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
//If you want to auto-create an invoice at order creation, uncomment:
|
||||
// $this->createInvoice($order);
|
||||
|
||||
return redirect()->route('admin.orders.show', $order->id)
|
||||
->with('success', 'Order created successfully.');
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// SHOW / POPUP
|
||||
// ---------------------------
|
||||
public function show($id)
|
||||
{
|
||||
$order = Order::with('items', 'markList')->findOrFail($id);
|
||||
@@ -61,7 +104,15 @@ class AdminOrderController extends Controller
|
||||
$user = User::where('customer_id', $order->markList->customer_id)->first();
|
||||
}
|
||||
|
||||
return view('admin.popup', compact('order', 'user'));
|
||||
$data['order_id'] = $order->id;
|
||||
|
||||
OrderItem::create($data);
|
||||
|
||||
// recalc totals and save to order
|
||||
$this->recalcTotals($order);
|
||||
// $this->updateInvoiceFromOrder($order); // <-- NEW
|
||||
|
||||
return redirect()->back()->with('success', 'Item added and totals updated.');
|
||||
}
|
||||
|
||||
/* ---------------------------
|
||||
@@ -599,56 +650,78 @@ class AdminOrderController extends Controller
|
||||
// 7) invoice number
|
||||
$invoiceNumber = $this->generateInvoiceNumber();
|
||||
|
||||
// 8) customer fetch
|
||||
$markList = MarkList::where('mark_no', $order->mark_no)->first();
|
||||
$customer = null;
|
||||
if ($markList && $markList->customer_id) {
|
||||
$customer = User::where('customer_id', $markList->customer_id)->first();
|
||||
}
|
||||
// 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);
|
||||
|
||||
// 9) invoice create
|
||||
$invoice = Invoice::create([
|
||||
'order_id' => $order->id,
|
||||
'customer_id' => $customer->id ?? null,
|
||||
'mark_no' => $order->mark_no,
|
||||
'invoice_number' => $invoiceNumber,
|
||||
'invoice_date' => now(),
|
||||
'due_date' => now()->addDays(10),
|
||||
'payment_method' => null,
|
||||
'reference_no' => null,
|
||||
'status' => 'pending',
|
||||
'final_amount' => $total_amount,
|
||||
'gst_percent' => 0,
|
||||
'gst_amount' => 0,
|
||||
'final_amount_with_gst' => $total_amount,
|
||||
'customer_name' => $customer->customer_name ?? null,
|
||||
'company_name' => $customer->company_name ?? null,
|
||||
'customer_email' => $customer->email ?? null,
|
||||
'customer_mobile' => $customer->mobile_no ?? null,
|
||||
'customer_address' => $customer->address ?? null,
|
||||
'pincode' => $customer->pincode ?? null,
|
||||
'notes' => null,
|
||||
'pdf_path' => null,
|
||||
]);
|
||||
// 2. Fetch customer (using mark list → customer_id)
|
||||
// $markList = MarkList::where('mark_no', $order->mark_no)->first();
|
||||
// $customer = null;
|
||||
|
||||
// 10) invoice items
|
||||
foreach ($order->items as $item) {
|
||||
InvoiceItem::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
'description' => $item->description,
|
||||
'ctn' => $item->ctn,
|
||||
'qty' => $item->qty,
|
||||
'ttl_qty' => $item->ttl_qty,
|
||||
'unit' => $item->unit,
|
||||
'price' => $item->price,
|
||||
'ttl_amount' => $item->ttl_amount,
|
||||
'cbm' => $item->cbm,
|
||||
'ttl_cbm' => $item->ttl_cbm,
|
||||
'kg' => $item->kg,
|
||||
'ttl_kg' => $item->ttl_kg,
|
||||
'shop_no' => $item->shop_no,
|
||||
]);
|
||||
}
|
||||
// if ($markList && $markList->customer_id) {
|
||||
// $customer = \App\Models\User::where('customer_id', $markList->customer_id)->first();
|
||||
// }
|
||||
|
||||
// 3. Create Invoice Record
|
||||
// $invoice = \App\Models\Invoice::create([
|
||||
// 'order_id' => $order->id,
|
||||
// 'customer_id' => $customer->id ?? null,
|
||||
// 'mark_no' => $order->mark_no,
|
||||
|
||||
// 'invoice_number' => $invoiceNumber,
|
||||
// 'invoice_date' => now(),
|
||||
// 'due_date' => now()->addDays(10),
|
||||
|
||||
// 'payment_method' => null,
|
||||
// 'reference_no' => null,
|
||||
// 'status' => 'pending',
|
||||
|
||||
// 'final_amount' => $total_amount,
|
||||
// 'gst_percent' => 0,
|
||||
// 'gst_amount' => 0,
|
||||
// 'final_amount_with_gst' => $total_amount,
|
||||
|
||||
// // snapshot customer fields
|
||||
// 'customer_name' => $customer->customer_name ?? null,
|
||||
// 'company_name' => $customer->company_name ?? null,
|
||||
// 'customer_email' => $customer->email ?? null,
|
||||
// 'customer_mobile' => $customer->mobile_no ?? null,
|
||||
// 'customer_address' => $customer->address ?? null,
|
||||
// 'pincode' => $customer->pincode ?? null,
|
||||
|
||||
// 'notes' => null,
|
||||
// ]);
|
||||
|
||||
// 4. Clone order items into invoice_items
|
||||
// foreach ($order->items as $item) {
|
||||
// \App\Models\InvoiceItem::create([
|
||||
// 'invoice_id' => $invoice->id,
|
||||
// 'description' => $item->description,
|
||||
// 'ctn' => $item->ctn,
|
||||
// 'qty' => $item->qty,
|
||||
// 'ttl_qty' => $item->ttl_qty,
|
||||
// 'unit' => $item->unit,
|
||||
// 'price' => $item->price,
|
||||
// 'ttl_amount' => $item->ttl_amount,
|
||||
// 'cbm' => $item->cbm,
|
||||
// 'ttl_cbm' => $item->ttl_cbm,
|
||||
// 'kg' => $item->kg,
|
||||
// 'ttl_kg' => $item->ttl_kg,
|
||||
// 'shop_no' => $item->shop_no,
|
||||
// ]);
|
||||
// }
|
||||
|
||||
// 5. TODO: PDF generation (I will add this later)
|
||||
// $invoice->pdf_path = null; // placeholder for now
|
||||
// $invoice->save();
|
||||
|
||||
// =======================
|
||||
// END INVOICE CREATION
|
||||
// =======================
|
||||
|
||||
// CLEAR TEMP DATA
|
||||
session()->forget(['temp_order_items', 'mark_no', 'origin', 'destination']);
|
||||
|
||||
return redirect()->route('admin.orders.index')
|
||||
->with('success', 'Order + Invoice created successfully.');
|
||||
|
||||
577
app/Http/Controllers/ContainerController.php
Normal file
577
app/Http/Controllers/ContainerController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -67,9 +67,12 @@ class ChatController extends Controller
|
||||
'sender_id' => auth()->id(),
|
||||
'sender_type' => \App\Models\User::class,
|
||||
'message' => $request->message,
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
'client_id' => $request->client_id, // ✅ ADD
|
||||
'read_by_admin' => false,
|
||||
'read_by_user' => true,
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
];
|
||||
|
||||
// Handle file upload
|
||||
@@ -86,7 +89,11 @@ class ChatController extends Controller
|
||||
$message->load('sender');
|
||||
|
||||
// Fire real-time event
|
||||
<<<<<<< HEAD
|
||||
broadcast(new NewChatMessage($message))->toOthers();
|
||||
=======
|
||||
broadcast(new NewChatMessage($message));
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
|
||||
@@ -10,6 +10,15 @@ class ChatMessage extends Model
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
<<<<<<< HEAD
|
||||
'ticket_id',
|
||||
'sender_id',
|
||||
'sender_type', // user OR admin
|
||||
'message',
|
||||
'file_path',
|
||||
'file_type',
|
||||
];
|
||||
=======
|
||||
'ticket_id',
|
||||
'sender_id',
|
||||
'sender_type',
|
||||
@@ -21,6 +30,7 @@ class ChatMessage extends Model
|
||||
'client_id',
|
||||
];
|
||||
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
|
||||
/**
|
||||
* The ticket this message belongs to.
|
||||
|
||||
30
app/Models/Container.php
Normal file
30
app/Models/Container.php
Normal 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);
|
||||
}
|
||||
}
|
||||
23
app/Models/ContainerRow.php
Normal file
23
app/Models/ContainerRow.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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,30 +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);
|
||||
}
|
||||
|
||||
// App\Models\Invoice.php
|
||||
|
||||
public function totalPaid()
|
||||
{
|
||||
return $this->installments()->sum('amount');
|
||||
}
|
||||
|
||||
public function remainingAmount()
|
||||
{
|
||||
return max(
|
||||
($this->final_amount_with_gst ?? 0) - $this->totalPaid(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
28
app/Models/LoadingListItem.php
Normal file
28
app/Models/LoadingListItem.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,8 @@
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"platform-check": false
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
|
||||
65
composer.lock
generated
65
composer.lock
generated
@@ -2880,6 +2880,18 @@
|
||||
},
|
||||
{
|
||||
"name": "maennchen/zipstream-php",
|
||||
<<<<<<< HEAD
|
||||
"version": "3.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maennchen/ZipStream-PHP.git",
|
||||
"reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/682f1098a8fddbaf43edac2306a691c7ad508ec5",
|
||||
"reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5",
|
||||
=======
|
||||
"version": "3.1.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
@@ -2890,21 +2902,34 @@
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f",
|
||||
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f",
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"ext-zlib": "*",
|
||||
<<<<<<< HEAD
|
||||
"php-64bit": "^8.3"
|
||||
=======
|
||||
"php-64bit": "^8.2"
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
},
|
||||
"require-dev": {
|
||||
"brianium/paratest": "^7.7",
|
||||
"ext-zip": "*",
|
||||
<<<<<<< HEAD
|
||||
"friendsofphp/php-cs-fixer": "^3.86",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"php-coveralls/php-coveralls": "^2.5",
|
||||
"phpunit/phpunit": "^12.0",
|
||||
=======
|
||||
"friendsofphp/php-cs-fixer": "^3.16",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"php-coveralls/php-coveralls": "^2.5",
|
||||
"phpunit/phpunit": "^11.0",
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
"vimeo/psalm": "^6.0"
|
||||
},
|
||||
"suggest": {
|
||||
@@ -2946,7 +2971,11 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
|
||||
<<<<<<< HEAD
|
||||
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.1"
|
||||
=======
|
||||
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2"
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2954,7 +2983,11 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
<<<<<<< HEAD
|
||||
"time": "2025-12-10T09:58:31+00:00"
|
||||
=======
|
||||
"time": "2025-01-27T12:07:53+00:00"
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
},
|
||||
{
|
||||
"name": "markbaker/complex",
|
||||
@@ -4181,6 +4214,18 @@
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
<<<<<<< HEAD
|
||||
"version": "1.30.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||
"reference": "09cdde5e2f078b9a3358dd217e2c8cb4dac84be2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/09cdde5e2f078b9a3358dd217e2c8cb4dac84be2",
|
||||
"reference": "09cdde5e2f078b9a3358dd217e2c8cb4dac84be2",
|
||||
=======
|
||||
"version": "1.30.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
@@ -4191,6 +4236,7 @@
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fa8257a579ec623473eabfe49731de5967306c4c",
|
||||
"reference": "fa8257a579ec623473eabfe49731de5967306c4c",
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4213,12 +4259,19 @@
|
||||
"markbaker/complex": "^3.0",
|
||||
"markbaker/matrix": "^3.0",
|
||||
"php": ">=7.4.0 <8.5.0",
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
|
||||
<<<<<<< HEAD
|
||||
"doctrine/instantiator": "^1.5",
|
||||
=======
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
"dompdf/dompdf": "^1.0 || ^2.0 || ^3.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.2",
|
||||
"mitoteam/jpgraph": "^10.3",
|
||||
@@ -4265,6 +4318,12 @@
|
||||
},
|
||||
{
|
||||
"name": "Adrien Crivelli"
|
||||
<<<<<<< HEAD
|
||||
},
|
||||
{
|
||||
"name": "Owen Leibman"
|
||||
=======
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
}
|
||||
],
|
||||
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
|
||||
@@ -4281,9 +4340,15 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
||||
<<<<<<< HEAD
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.2"
|
||||
},
|
||||
"time": "2026-01-11T05:58:24+00:00"
|
||||
=======
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.1"
|
||||
},
|
||||
"time": "2025-10-26T16:01:04+00:00"
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
|
||||
@@ -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,
|
||||
|
||||
// ✅ Laravel‑Excel facade
|
||||
'Excel' => Maatwebsite\Excel\Facades\Excel::class,
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
380
config/excel.php
Normal file
380
config/excel.php
Normal 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,
|
||||
],
|
||||
];
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
BIN
public/invoices/invoice-INV-2026-000045.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000045.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000054.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000054.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000055.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000055.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000056.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000056.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000058.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000058.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000060.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000060.pdf
Normal file
Binary file not shown.
@@ -1,30 +1,31 @@
|
||||
import Echo from "laravel-echo";
|
||||
import Pusher from "pusher-js";
|
||||
import Echo from 'laravel-echo';
|
||||
import Pusher from 'pusher-js';
|
||||
|
||||
window.Pusher = Pusher;
|
||||
|
||||
console.log("[ECHO] Initializing Reverb...");
|
||||
|
||||
// Get CSRF token from meta tag
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
window.Echo = new Echo({
|
||||
broadcaster: "reverb",
|
||||
broadcaster: 'reverb',
|
||||
key: import.meta.env.VITE_REVERB_APP_KEY,
|
||||
|
||||
wsHost: import.meta.env.VITE_REVERB_HOST,
|
||||
wsPort: Number(import.meta.env.VITE_REVERB_PORT),
|
||||
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'],
|
||||
|
||||
forceTLS: false,
|
||||
disableStats: true,
|
||||
authEndpoint: '/admin/broadcasting/auth',
|
||||
|
||||
authEndpoint: "/broadcasting/auth",
|
||||
|
||||
// ⭐ MOST IMPORTANT ⭐
|
||||
withCredentials: true,
|
||||
|
||||
auth: {
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": document
|
||||
.querySelector('meta[name="csrf-token"]')
|
||||
?.getAttribute("content"),
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
},
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[ECHO] Loaded Successfully!", window.Echo);
|
||||
console.log('%c[ECHO] Initialized!', 'color: green; font-weight: bold;', window.Echo);
|
||||
|
||||
@@ -1,548 +1,96 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Chat Support Dashboard')
|
||||
@section('page-title', 'Chat Support')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
:root {
|
||||
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--success-gradient: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
--danger-gradient: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%);
|
||||
--warning-gradient: linear-gradient(135deg, #f7971e 0%, #ffd200 100%);
|
||||
--info-gradient: linear-gradient(135deg, #56ccf2 0%, #2f80ed 100%);
|
||||
--card-shadow: 0 3px 10px rgba(0,0,0,0.05);
|
||||
--card-shadow-hover: 0 6px 16px rgba(0,0,0,0.10);
|
||||
--border-radius: 8px;
|
||||
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
<div class="container py-4">
|
||||
|
||||
.chat-dashboard {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 0.75rem;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
<h2 class="mb-4 fw-bold">Customer Support Chat</h2>
|
||||
|
||||
.dashboard-header {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: clamp(1.4rem, 2.5vw, 2rem);
|
||||
font-weight: 800;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin: 0 0 0.4rem 0;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dashboard-title::before {
|
||||
content: '💬';
|
||||
position: absolute;
|
||||
left: -1.6rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 1.3rem;
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% { transform: translateY(-50%) translateY(0); }
|
||||
40% { transform: translateY(-50%) translateY(-5px); }
|
||||
60% { transform: translateY(-50%) translateY(-2px); }
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
color: #64748b;
|
||||
font-size: 0.8rem;
|
||||
max-width: 380px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* 🔔 GLOBAL NEW MESSAGE COUNTER */
|
||||
.global-notify {
|
||||
margin: 0 auto 0.75rem auto;
|
||||
max-width: 320px;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(15,23,42,0.03);
|
||||
border: 1px dashed #cbd5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.78rem;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.global-notify-badge {
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
min-width: 18px;
|
||||
padding: 0 0.35rem;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 0 0 2px rgba(254, 226, 226, 0.8);
|
||||
}
|
||||
|
||||
.global-notify.d-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tickets-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.75rem 0.9rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
/* max-height / overflow काढले, जेणेकरून बाहेरचा page scroll वापरला जाईल */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tickets-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.6rem;
|
||||
padding-bottom: 0.45rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.tickets-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.tickets-count {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ticket-item {
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.6rem;
|
||||
margin-bottom: 0.3rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: var(--primary-gradient);
|
||||
transform: scaleY(0);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.ticket-item:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--card-shadow-hover);
|
||||
border-color: rgba(102, 126, 234, 0.35);
|
||||
}
|
||||
|
||||
.ticket-item:hover::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.ticket-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.ticket-avatar {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 6px;
|
||||
background: var(--info-gradient);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 3px 8px rgba(86, 204, 242, 0.25);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-avatar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
transition: left 0.4s;
|
||||
}
|
||||
|
||||
.ticket-item:hover .ticket-avatar::after {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.ticket-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ticket-name {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 0 0 0.08rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.unread-count {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 6px rgba(239, 68, 68, 0.4);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.ticket-preview {
|
||||
color: #64748b;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.25;
|
||||
margin-bottom: 0.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
max-height: 1.8em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-time {
|
||||
font-size: 0.65rem;
|
||||
color: #94a3b8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
.ticket-time svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ticket-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 0.3rem;
|
||||
border-top: 1px dashed #e5e7eb;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.14rem 0.45rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.status-open { background: var(--success-gradient); color: white; }
|
||||
.status-closed{ background: var(--danger-gradient); color: white; }
|
||||
|
||||
.chat-btn {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
font-size: 0.7rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.25);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.35);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat-btn::after {
|
||||
content: '→';
|
||||
transition: margin-left 0.25s ease;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.chat-btn:hover::after {
|
||||
margin-left: 0.18rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 1.6rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: var(--border-radius);
|
||||
border: 2px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 2.8rem;
|
||||
margin-bottom: 0.9rem;
|
||||
display: block;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.empty-subtitle {
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.ticket-id {
|
||||
background: rgba(102, 126, 234, 0.08);
|
||||
color: #667eea;
|
||||
padding: 0.12rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
.new-message-dot {
|
||||
position: absolute;
|
||||
top: 0.45rem;
|
||||
right: 0.5rem;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: #ef4444;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.3);
|
||||
animation: blink 1.5s infinite;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(1.1); }
|
||||
}
|
||||
|
||||
/* इथे आता inner scroll नाही */
|
||||
.tickets-list {
|
||||
/* flex: 1; काढला, overflow काढला, parent + body scroll वापरेल */
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar-thumb {
|
||||
background: var(--primary-gradient);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chat-dashboard {
|
||||
padding: 0.6rem;
|
||||
}
|
||||
|
||||
.tickets-container {
|
||||
/* max-height काढलेले, mobile वरही outer scroll */
|
||||
}
|
||||
|
||||
.ticket-header {
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.ticket-footer {
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.chat-btn {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="chat-dashboard">
|
||||
<!-- Header -->
|
||||
<div class="dashboard-header">
|
||||
<h1 class="dashboard-title">Live Chat Dashboard</h1>
|
||||
<p class="dashboard-subtitle">
|
||||
Monitor customer conversations with real-time updates
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 🔔 GLOBAL NEW MESSAGES NOTIFICATION -->
|
||||
<div id="globalNewMessageBox" class="global-notify d-none">
|
||||
<span>New messages:</span>
|
||||
<span id="globalNewMessageCount" class="global-notify-badge">0</span>
|
||||
</div>
|
||||
|
||||
<!-- Tickets Container -->
|
||||
<div class="tickets-container">
|
||||
<div class="tickets-header">
|
||||
<div>
|
||||
<h2 class="tickets-title">
|
||||
📋 Active Conversations
|
||||
<span class="tickets-count">{{ $tickets->count() }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
|
||||
@if($tickets->count() === 0)
|
||||
<!-- Empty State -->
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">💬</div>
|
||||
<h3 class="empty-title">No Active Conversations</h3>
|
||||
<p class="empty-subtitle">
|
||||
Customer conversations will appear here with real-time notifications.
|
||||
</p>
|
||||
<div class="ticket-id">Ready for support requests</div>
|
||||
<div class="p-4 text-center text-muted">
|
||||
<h5>No customer chats yet.</h5>
|
||||
</div>
|
||||
@else
|
||||
<!-- Tickets List -->
|
||||
<div class="tickets-list">
|
||||
<ul class="list-group list-group-flush">
|
||||
|
||||
@foreach($tickets as $ticket)
|
||||
<div class="ticket-item" data-ticket-id="{{ $ticket->id }}">
|
||||
@if($ticket->unread_count > 0)
|
||||
<div class="new-message-dot"></div>
|
||||
@endif
|
||||
|
||||
<div class="ticket-header">
|
||||
<div class="ticket-avatar">
|
||||
{{ strtoupper(substr($ticket->user->customer_name ?? $ticket->user->name, 0, 1)) }}
|
||||
</div>
|
||||
<div class="ticket-content">
|
||||
<div class="ticket-name">
|
||||
{{ $ticket->user->customer_name ?? $ticket->user->name }}
|
||||
@if($ticket->unread_count > 0)
|
||||
<span id="badge-{{ $ticket->id }}" class="unread-count">
|
||||
{{ $ticket->unread_count }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@php
|
||||
// Get last message
|
||||
$lastMsg = $ticket->messages()->latest()->first();
|
||||
@endphp
|
||||
|
||||
<div class="ticket-preview">
|
||||
<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, 45) }}
|
||||
@elseif(Str::startsWith($lastMsg->file_type, 'image'))
|
||||
📷 Photo shared
|
||||
{{ Str::limit($lastMsg->message, 35) }}
|
||||
@elseif($lastMsg->file_type === 'image')
|
||||
📷 Image
|
||||
@elseif($lastMsg->file_type === 'video')
|
||||
🎥 Video
|
||||
@else
|
||||
📎 File attached
|
||||
📎 Attachment
|
||||
@endif
|
||||
@else
|
||||
<em>Conversation started</em>
|
||||
<i>No messages yet</i>
|
||||
@endif
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@if($lastMsg)
|
||||
<div class="ticket-time">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
|
||||
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
|
||||
</svg>
|
||||
{{ $lastMsg->created_at->diffForHumans() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ticket-footer">
|
||||
<span class="status-badge status-{{ $ticket->status }}">
|
||||
<!-- Right Side: Status + Button -->
|
||||
<div class="text-end">
|
||||
|
||||
<!-- Ticket Status -->
|
||||
<span class="badge
|
||||
{{ $ticket->status === 'open' ? 'bg-success' : 'bg-danger' }}">
|
||||
{{ ucfirst($ticket->status) }}
|
||||
</span>
|
||||
<a href="{{ route('admin.chat.open', $ticket->id) }}" class="chat-btn">
|
||||
Open Chat
|
||||
|
||||
<!-- 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
|
||||
</div>
|
||||
|
||||
</ul>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
710
resources/views/admin/container.blade.php
Normal file
710
resources/views/admin/container.blade.php
Normal 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
|
||||
251
resources/views/admin/container_create.blade.php
Normal file
251
resources/views/admin/container_create.blade.php
Normal 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
|
||||
291
resources/views/admin/container_show.blade.php
Normal file
291
resources/views/admin/container_show.blade.php
Normal 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 FRONT‑END SEARCH --}}
|
||||
<script>
|
||||
function cmFilterRows() {
|
||||
const input = document.getElementById('cmRowSearch');
|
||||
if (!input) return;
|
||||
const filter = input.value.toLowerCase();
|
||||
const table = document.getElementById('cmExcelTable');
|
||||
if (!table) return;
|
||||
|
||||
const rows = table.getElementsByTagName('tr');
|
||||
for (let i = 1; i < rows.length; i++) { // skip header
|
||||
const cells = rows[i].getElementsByTagName('td');
|
||||
let match = false;
|
||||
for (let j = 0; j < cells.length; j++) {
|
||||
const txt = cells[j].textContent || cells[j].innerText;
|
||||
if (txt.toLowerCase().indexOf(filter) > -1) {
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
rows[i].style.display = match ? '' : 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,11 +5,17 @@
|
||||
<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 {
|
||||
@@ -22,7 +28,6 @@
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* ✨ Sidebar Glass + Animated Highlight Effect */
|
||||
.sidebar {
|
||||
width: 200px;
|
||||
height: 100vh;
|
||||
@@ -41,7 +46,6 @@
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Sidebar collapsed state */
|
||||
.sidebar.collapsed {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
@@ -64,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;
|
||||
@@ -84,7 +89,6 @@
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Background Animation */
|
||||
.sidebar a::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -109,7 +113,6 @@
|
||||
color: #1258e0 !important;
|
||||
}
|
||||
|
||||
/* Icon bounce effect */
|
||||
.sidebar a i {
|
||||
margin-right: 8px;
|
||||
font-size: 1.1rem;
|
||||
@@ -121,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;
|
||||
@@ -136,7 +138,6 @@
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
/* Logout Button */
|
||||
.sidebar form button {
|
||||
border-radius: 12px;
|
||||
margin-top: 12px;
|
||||
@@ -150,7 +151,6 @@
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
/* 🧭 Main Content */
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
min-height: 100vh;
|
||||
@@ -162,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;
|
||||
@@ -254,7 +252,10 @@ header .bi-bell .badge {
|
||||
<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) --}}
|
||||
@@ -263,12 +264,19 @@ header .bi-bell .badge {
|
||||
<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 --}}
|
||||
@@ -292,10 +300,12 @@ header .bi-bell .badge {
|
||||
</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')
|
||||
@@ -318,10 +328,12 @@ header .bi-bell .badge {
|
||||
</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')
|
||||
@@ -354,21 +366,30 @@ header .bi-bell .badge {
|
||||
<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>
|
||||
@@ -383,13 +404,11 @@ header .bi-bell .badge {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
/* ALL YOUR EXISTING CSS STAYS HERE - EXCEPT GST TOTALS SECTION REMOVED */
|
||||
:root {
|
||||
@@ -22,7 +24,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;
|
||||
@@ -31,14 +33,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;
|
||||
}
|
||||
@@ -61,7 +63,7 @@
|
||||
}
|
||||
|
||||
.id-box {
|
||||
background: white;
|
||||
background: #ffffff;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1rem;
|
||||
border: 1px solid #e9ecef;
|
||||
@@ -85,7 +87,6 @@
|
||||
.id-box-secondary {
|
||||
border-left: 4px solid var(--success);
|
||||
}
|
||||
|
||||
.id-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
@@ -93,8 +94,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.id-icon-primary {
|
||||
@@ -106,11 +107,6 @@
|
||||
background: linear-gradient(135deg, #27ae60 0%, #219653 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.id-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.id-label {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
@@ -129,73 +125,64 @@
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Enhanced Date Section with Blue-Purple Gradient */
|
||||
.date-badge {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
background: linear-gradient(135deg, #3498db 0%, #9b59b6 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
min-width: 140px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.2);
|
||||
.date-container {
|
||||
background: #ffffff;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #e9ecef;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
.date-badge:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.3);
|
||||
.date-card {
|
||||
text-align: center;
|
||||
padding: 0.75rem;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.date-badge .badge-label {
|
||||
font-size: 0.7rem;
|
||||
.date-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 0.5rem;
|
||||
background: var(--secondary);
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.date-label {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.date-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
padding: 0.5rem;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.date-connector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.date-badge .badge-label i {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.date-badge .badge-value {
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.date-badge.due-date {
|
||||
background: linear-gradient(135deg, #3498db 0%, #9b59b6 100%);
|
||||
}
|
||||
|
||||
.date-badge.overdue {
|
||||
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.4); }
|
||||
70% { box-shadow: 0 0 0 6px rgba(231, 76, 60, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(231, 76, 60, 0); }
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
color: #dee2e6;
|
||||
font-weight: 300;
|
||||
padding: 0 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.date-separator i {
|
||||
background: white;
|
||||
padding: 8px;
|
||||
.date-connector i {
|
||||
background: var(--light);
|
||||
padding: 10px;
|
||||
border-radius: 50%;
|
||||
color: var(--secondary);
|
||||
border: 2px solid #e9ecef;
|
||||
@@ -267,7 +254,6 @@
|
||||
padding: 0.35rem 0.65rem;
|
||||
}
|
||||
|
||||
/* COMPACT HEADER STYLES */
|
||||
.compact-header {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
@@ -276,22 +262,19 @@
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Date and status row */
|
||||
.date-status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
.compact-header .status-badge {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* GST Totals Section Styles - COMPLETELY REMOVED */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.invoice-container {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.date-connector {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
@@ -365,15 +348,11 @@
|
||||
<body>
|
||||
<div class="invoice-container">
|
||||
<div class="p-4">
|
||||
<!-- ============================
|
||||
INVOICE HEADER - COMPACT
|
||||
============================ -->
|
||||
@php
|
||||
$showActions = $showActions ?? true;
|
||||
|
||||
// REMOVED GST CALCULATION LOGIC
|
||||
@endphp
|
||||
|
||||
{{-- HEADER --}}
|
||||
<div class="compact-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
@@ -382,26 +361,7 @@
|
||||
</h2>
|
||||
<h4 class="fw-bold text-dark mb-0">{{ $invoice->invoice_number }}</h4>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 text-end">
|
||||
<div class="date-status-row">
|
||||
<!-- Invoice Date -->
|
||||
<div class="date-badge">
|
||||
<div class="badge-label">
|
||||
<i class="fas fa-calendar-alt"></i> INVOICE DATE
|
||||
</div>
|
||||
<div class="badge-value">{{ \Carbon\Carbon::parse($invoice->invoice_date)->format('M d, Y') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Due Date -->
|
||||
<div class="date-badge due-date @if($invoice->status == 'overdue') overdue @endif">
|
||||
<div class="badge-label">
|
||||
<i class="fas fa-clock"></i> DUE DATE
|
||||
</div>
|
||||
<div class="badge-value">{{ \Carbon\Carbon::parse($invoice->due_date)->format('M d, Y') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Badge -->
|
||||
<span class="status-badge
|
||||
@if($invoice->status == 'paid') bg-success
|
||||
@elseif($invoice->status == 'overdue') bg-danger
|
||||
@@ -417,64 +377,75 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================
|
||||
ORDER & SHIPMENT ID BOXES
|
||||
============================ -->
|
||||
{{-- ID BOXES: INVOICE + CONTAINER --}}
|
||||
<div class="id-container">
|
||||
<div class="row">
|
||||
<!-- Order ID Box -->
|
||||
{{-- 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-shopping-cart"></i>
|
||||
<i class="fas fa-receipt"></i>
|
||||
</div>
|
||||
<div class="id-content">
|
||||
<div class="id-label">ORDER ID</div>
|
||||
<div class="id-label">Invoice ID</div>
|
||||
<div class="id-value">{{ $invoice->invoice_number }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 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-box"></i>
|
||||
</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
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shipment 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-shipping-fast"></i>
|
||||
|
||||
{{-- DATES --}}
|
||||
<div class="date-container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-5">
|
||||
<div class="date-card">
|
||||
<div class="date-icon">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
</div>
|
||||
<div class="id-content">
|
||||
<div class="id-label">SHIPMENT ID</div>
|
||||
<div class="id-value">
|
||||
@php
|
||||
$shipmentId = 'N/A';
|
||||
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 class="date-label">INVOICE DATE</div>
|
||||
<div class="date-value">
|
||||
{{ \Carbon\Carbon::parse($invoice->invoice_date)->format('M d, Y') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="date-connector">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="date-card">
|
||||
<div class="date-icon">
|
||||
<i class="fas fa-clock"></i>
|
||||
</div>
|
||||
<div class="date-label">DUE DATE</div>
|
||||
<div class="date-value @if($invoice->status == 'overdue') text-danger @endif">
|
||||
{{ \Carbon\Carbon::parse($invoice->due_date)->format('M d, Y') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================
|
||||
CUSTOMER DETAILS
|
||||
============================ -->
|
||||
{{-- CUSTOMER DETAILS --}}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0 fw-bold">
|
||||
@@ -510,9 +481,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">
|
||||
@@ -520,6 +493,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">
|
||||
@@ -548,8 +528,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>
|
||||
@@ -559,43 +566,127 @@
|
||||
</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>
|
||||
|
||||
<!-- ============================
|
||||
GST TOTALS SECTION - COMPLETELY REMOVED
|
||||
============================ -->
|
||||
{{-- FINAL SUMMARY --}}
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-6">
|
||||
<div class="card summary-card">
|
||||
<div class="card-header summary-header">
|
||||
<h6 class="mb-0 fw-bold">
|
||||
<i class="fas fa-calculator me-2"></i> Final Summary
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">Amount:</span>
|
||||
<span class="fw-bold text-dark">
|
||||
₹{{ number_format($invoice->final_amount, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- ============================
|
||||
FOOTER — DOWNLOAD & SHARE
|
||||
============================ -->
|
||||
<!-- <div class="mt-4 pt-3 border-top text-center">
|
||||
<a href="{{ route('admin.invoices.download', $invoice->id) }}"
|
||||
class="btn btn-primary me-2">
|
||||
@if($invoice->tax_type === 'gst')
|
||||
{{-- CGST --}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">
|
||||
CGST ({{ $invoice->cgst_percent ?? ($invoice->gst_percent / 2) }}%):
|
||||
</span>
|
||||
<span class="fw-bold text-danger">
|
||||
₹{{ number_format($invoice->gst_amount / 2, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{-- SGST --}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">
|
||||
SGST ({{ $invoice->sgst_percent ?? ($invoice->gst_percent / 2) }}%):
|
||||
</span>
|
||||
<span class="fw-bold text-danger">
|
||||
₹{{ number_format($invoice->gst_amount / 2, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
@elseif($invoice->tax_type === 'igst')
|
||||
{{-- IGST --}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">
|
||||
IGST ({{ $invoice->igst_percent ?? $invoice->gst_percent }}%):
|
||||
</span>
|
||||
<span class="fw-bold text-danger">
|
||||
₹{{ number_format($invoice->gst_amount, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
@else
|
||||
{{-- Default GST --}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">
|
||||
GST ({{ $invoice->gst_percent }}%):
|
||||
</span>
|
||||
<span class="fw-bold text-danger">
|
||||
₹{{ number_format($invoice->gst_amount, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center pt-1">
|
||||
<span class="fw-bold text-dark">Total Payable:</span>
|
||||
<span class="fw-bold text-success">
|
||||
₹{{ number_format($invoice->final_amount_with_gst, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- FOOTER ACTIONS --}}
|
||||
<div class="mt-4 pt-3 border-top text-center">
|
||||
@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>
|
||||
</div> -->
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- FOOTER MESSAGE --}}
|
||||
<div class="mt-4 pt-3 border-top text-center text-muted">
|
||||
<p class="mb-1">Thank you for your business!</p>
|
||||
<p class="mb-0">For any inquiries, contact us at support@Kent Logistic</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- ============================
|
||||
SHARE SCRIPT
|
||||
============================ -->
|
||||
<!-- <script>
|
||||
<script>
|
||||
function shareInvoice() {
|
||||
const shareData = {
|
||||
title: "Invoice {{ $invoice->invoice_number }}",
|
||||
text: "Sharing invoice {{ $invoice->invoice_number }}",
|
||||
url: "{{ route('admin.invoices.download', $invoice->id) }}"
|
||||
url: "{{ asset($invoice->pdf_path) }}"
|
||||
};
|
||||
|
||||
if (navigator.share) {
|
||||
@@ -605,12 +696,7 @@
|
||||
alert("Link copied! Sharing not supported on this browser.");
|
||||
}
|
||||
}
|
||||
|
||||
// Add print functionality
|
||||
function printInvoice() {
|
||||
window.print();
|
||||
}
|
||||
</script> -->
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -12,6 +12,15 @@ use App\Http\Controllers\MarkListController;
|
||||
use App\Http\Controllers\User\UserOrderController;
|
||||
use App\Http\Controllers\User\UserProfileController;
|
||||
use App\Http\Controllers\User\ChatController;
|
||||
<<<<<<< HEAD
|
||||
|
||||
|
||||
|
||||
Broadcast::routes(['middleware' => ['auth:api']]);
|
||||
|
||||
|
||||
=======
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
|
||||
//user send request
|
||||
Route::post('/signup-request', [RequestController::class, 'usersignup']);
|
||||
@@ -55,7 +64,11 @@ Route::middleware(['auth:api'])->group(function () {
|
||||
|
||||
// Route::post('/user/profile/update', [UserProfileController::class, 'updateProfile']);
|
||||
|
||||
<<<<<<< HEAD
|
||||
// ===========================
|
||||
=======
|
||||
// ===========================
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
// CHAT SUPPORT ROUTES
|
||||
// ===========================
|
||||
Route::get('/user/chat/start', [ChatController::class, 'startChat']);
|
||||
@@ -66,6 +79,15 @@ Route::middleware(['auth:api'])->group(function () {
|
||||
});
|
||||
|
||||
|
||||
<<<<<<< HEAD
|
||||
Route::post('/broadcasting/auth', function (Request $request) {
|
||||
if (!auth()->check()) {
|
||||
return response()->json(['message' => 'Unauthenticated'], 401);
|
||||
}
|
||||
|
||||
return Broadcast::auth($request);
|
||||
})->middleware('auth:api');
|
||||
=======
|
||||
|
||||
|
||||
|
||||
@@ -91,3 +113,4 @@ Route::post('/broadcasting/auth', function (Request $request) {
|
||||
|
||||
|
||||
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
|
||||
21
routes/broadcasting.php
Normal file
21
routes/broadcasting.php
Normal 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']);
|
||||
@@ -2,77 +2,41 @@
|
||||
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
use App\Models\SupportTicket;
|
||||
use App\Models\Admin;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
file_put_contents(storage_path('logs/broadcast_debug.log'), now()." CHANNELS LOADED\n", FILE_APPEND);
|
||||
|
||||
Broadcast::routes([
|
||||
'middleware' => ['web', 'auth:admin'],
|
||||
]);
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Broadcast Channels
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
Broadcast::channel('ticket.{ticketId}', function ($user, $ticketId) {
|
||||
|
||||
try {
|
||||
// Very explicit logging to see what arrives here
|
||||
Log::info("CHANNEL AUTH CHECK (ENTER)", [
|
||||
'user_present' => $user !== null,
|
||||
'user_type' => is_object($user) ? get_class($user) : gettype($user),
|
||||
'user_id' => $user->id ?? null,
|
||||
\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)
|
||||
]);
|
||||
|
||||
// Find ticket and log
|
||||
$ticket = SupportTicket::find($ticketId);
|
||||
Log::info("CHANNEL AUTH: found ticket", [
|
||||
'ticket_exists' => $ticket ? true : false,
|
||||
'ticket_id' => $ticket?->id,
|
||||
'ticket_user_id' => $ticket?->user_id,
|
||||
]);
|
||||
|
||||
if (!$ticket) {
|
||||
Log::warning("CHANNEL AUTH: ticket not found", ['ticketId' => $ticketId]);
|
||||
\Log::warning('❌ Ticket not found', ['ticketId' => $ticketId]);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If admin, allow
|
||||
if ($user instanceof Admin) {
|
||||
Log::info("CHANNEL AUTH: admin allowed", ['admin_id' => $user->id]);
|
||||
// ✅ Admin/Staff Check (Session Auth)
|
||||
if (get_class($user) === 'App\Models\Admin') {
|
||||
\Log::info('✅ Admin authorized for ticket', ['admin_id' => $user->id]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If normal user, check ownership
|
||||
if (is_object($user) && isset($user->id)) {
|
||||
$allowed = $ticket->user_id === $user->id;
|
||||
Log::info("CHANNEL AUTH: user allowed check", [
|
||||
'ticket_user_id' => $ticket->user_id,
|
||||
'current_user_id' => $user->id,
|
||||
'allowed' => $allowed
|
||||
]);
|
||||
return $allowed;
|
||||
// ✅ 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("CHANNEL AUTH: default deny");
|
||||
\Log::warning('❌ Authorization failed');
|
||||
return false;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("CHANNEL AUTH ERROR", [
|
||||
'message' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
Broadcast::channel('admin.chat', function ($admin) {
|
||||
return auth('admin')->check();
|
||||
});
|
||||
|
||||
// Broadcast::channel('ticket.{ticketId}', function ($admin, $ticketId) {
|
||||
// \Log::info('CHANNEL AUTH OK', [
|
||||
// 'admin_id' => $admin->id,
|
||||
// 'ticketId' => $ticketId,
|
||||
// ]);
|
||||
|
||||
// return true;
|
||||
// });
|
||||
54
routes/container.blade.php
Normal file
54
routes/container.blade.php
Normal 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
|
||||
@@ -24,6 +24,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
|
||||
// ---------------------------
|
||||
@@ -41,7 +76,11 @@ Route::get('/login', function () {
|
||||
|
||||
|
||||
|
||||
//Broadcast::routes([
|
||||
//'middleware' => ['web', 'auth:admin'],
|
||||
//]);
|
||||
|
||||
Broadcast::routes(['middleware' => ['web']]);
|
||||
// ==========================================
|
||||
// PROTECTED ADMIN ROUTES (session protected)
|
||||
// ==========================================
|
||||
@@ -68,6 +107,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
|
||||
// ---------------------------
|
||||
@@ -239,6 +305,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');
|
||||
|
||||
@@ -355,3 +426,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');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,9 +8,8 @@ export default defineConfig({
|
||||
input: [
|
||||
"resources/css/app.css",
|
||||
"resources/js/app.js",
|
||||
"resources/js/echo.js",
|
||||
// ❌ Remove echo.js from here - it's imported in app.js
|
||||
],
|
||||
|
||||
refresh: true,
|
||||
}),
|
||||
tailwindcss(),
|
||||
|
||||
Reference in New Issue
Block a user