Add Container field
This commit is contained in:
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
|
||||
|
||||
87
app/Events/NewChatMessage.php
Normal file
87
app/Events/NewChatMessage.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\ChatMessage;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class NewChatMessage implements ShouldBroadcast
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public $message;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(ChatMessage $message)
|
||||
{
|
||||
// Safe data only (no heavy relationships in queue)
|
||||
$this->message = [
|
||||
'id' => $message->id,
|
||||
'ticket_id' => $message->ticket_id,
|
||||
'sender_id' => $message->sender_id,
|
||||
'sender_type' => $message->sender_type,
|
||||
'message' => $message->message,
|
||||
'file_path' => $message->file_path,
|
||||
'file_type' => $message->file_type,
|
||||
'created_at' => $message->created_at->toDateTimeString(),
|
||||
];
|
||||
|
||||
// Load sender separately for broadcastWith()
|
||||
$this->sender = $message->sender;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The channel the event should broadcast on.
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new PrivateChannel('ticket.' . $this->message->ticket_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data sent to frontend (Blade + Flutter)
|
||||
*/
|
||||
public function broadcastWith()
|
||||
{
|
||||
\Log::info("DEBUG: NewChatMessage broadcasting on channel ticket.".$this->message->ticket_id);
|
||||
|
||||
return [
|
||||
'id' => $this->message->id,
|
||||
'ticket_id' => $this->message->ticket_id,
|
||||
'sender_id' => $this->message->sender_id,
|
||||
'sender_type' => $this->message->sender_type,
|
||||
'message' => $this->message->message,
|
||||
'file_url' => $this->message->file_path
|
||||
? asset('storage/' . $this->message->file_path)
|
||||
: null,
|
||||
'file_type' => $this->message->file_type,
|
||||
'sender' => [
|
||||
'id' => $this->message->sender->id,
|
||||
'name' => $this->getSenderName(),
|
||||
'is_admin' => $this->message->sender_type === \App\Models\Admin::class,
|
||||
],
|
||||
'created_at' => $this->message->created_at->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to extract sender name
|
||||
*/
|
||||
private function getSenderName()
|
||||
{
|
||||
$sender = $this->message->sender;
|
||||
|
||||
// User has customer_name (in your app)
|
||||
if ($this->message->sender_type === \App\Models\User::class) {
|
||||
return $sender->customer_name ?? $sender->name ?? "User";
|
||||
}
|
||||
|
||||
// Admin model has ->name
|
||||
return $sender->name ?? "Admin";
|
||||
}
|
||||
}
|
||||
85
app/Http/Controllers/Admin/AdminChatController.php
Normal file
85
app/Http/Controllers/Admin/AdminChatController.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\SupportTicket;
|
||||
use App\Models\ChatMessage;
|
||||
use App\Events\NewChatMessage;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AdminChatController extends Controller
|
||||
{
|
||||
/**
|
||||
* Page 1: List all customer chat tickets
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$tickets = SupportTicket::with(['user', 'messages' => function($query) {
|
||||
$query->latest()->limit(1);
|
||||
}])
|
||||
->orderBy('updated_at', 'desc')
|
||||
->get();
|
||||
|
||||
return view('admin.chat_support', compact('tickets'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Page 2: Open chat window for a specific user
|
||||
*/
|
||||
public function openChat($ticketId)
|
||||
{
|
||||
$ticket = SupportTicket::with('user')->findOrFail($ticketId);
|
||||
$messages = ChatMessage::where('ticket_id', $ticketId)
|
||||
->orderBy('created_at', 'asc')
|
||||
->with('sender')
|
||||
->get();
|
||||
|
||||
return view('admin.chat_window', compact('ticket', 'messages'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin sends a message to the user (FIXED - LIVE CHAT)
|
||||
*/
|
||||
public function sendMessage(Request $request, $ticketId)
|
||||
{
|
||||
$request->validate([
|
||||
'message' => 'nullable|string',
|
||||
'file' => 'nullable|file|max:20480', // 20 MB
|
||||
]);
|
||||
|
||||
$ticket = SupportTicket::findOrFail($ticketId);
|
||||
$admin = auth('admin')->user();
|
||||
|
||||
$data = [
|
||||
'ticket_id' => $ticketId,
|
||||
'sender_id' => $admin->id,
|
||||
'sender_type' => \App\Models\Admin::class,
|
||||
'message' => $request->message,
|
||||
];
|
||||
|
||||
// File Upload
|
||||
if ($request->hasFile('file')) {
|
||||
$path = $request->file('file')->store('chat', 'public');
|
||||
$data['file_path'] = $path;
|
||||
$data['file_type'] = $request->file('file')->getMimeType();
|
||||
}
|
||||
|
||||
// Save message
|
||||
$message = ChatMessage::create($data);
|
||||
$message->load('sender');
|
||||
|
||||
\Log::info("DEBUG: ChatController sendMessage called", [
|
||||
'ticket_id' => $ticketId,
|
||||
'payload' => $request->all()
|
||||
]);
|
||||
|
||||
// 🔥 LIVE CHAT - Queue bypass (100% working)
|
||||
broadcast(new NewChatMessage($message))->toOthers();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use Mpdf\Mpdf;
|
||||
use App\Models\InvoiceInstallment;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Mpdf\Mpdf;
|
||||
|
||||
class AdminInvoiceController extends Controller
|
||||
{
|
||||
@@ -17,7 +17,10 @@ class AdminInvoiceController extends Controller
|
||||
// -------------------------------------------------------------
|
||||
public function index()
|
||||
{
|
||||
$invoices = Invoice::with(['order.shipments'])->latest()->get();
|
||||
$invoices = Invoice::with(['items', 'customer', 'container'])
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
return view('admin.invoice', compact('invoices'));
|
||||
}
|
||||
|
||||
@@ -26,13 +29,8 @@ class AdminInvoiceController extends Controller
|
||||
// -------------------------------------------------------------
|
||||
public function popup($id)
|
||||
{
|
||||
$invoice = Invoice::with(['items', 'order'])->findOrFail($id);
|
||||
|
||||
// Find actual Shipment record
|
||||
$shipment = \App\Models\Shipment::whereHas('items', function ($q) use ($invoice) {
|
||||
$q->where('order_id', $invoice->order_id);
|
||||
})
|
||||
->first();
|
||||
$invoice = Invoice::with(['items', 'customer', 'container'])->findOrFail($id);
|
||||
$shipment = null;
|
||||
|
||||
return view('admin.popup_invoice', compact('invoice', 'shipment'));
|
||||
}
|
||||
@@ -42,24 +40,28 @@ class AdminInvoiceController extends Controller
|
||||
// -------------------------------------------------------------
|
||||
public function edit($id)
|
||||
{
|
||||
$invoice = Invoice::with(['order.shipments'])->findOrFail($id);
|
||||
$shipment = $invoice->order?->shipments?->first();
|
||||
$invoice = Invoice::with(['items', 'customer', 'container'])->findOrFail($id);
|
||||
$shipment = null;
|
||||
|
||||
return view('admin.invoice_edit', compact('invoice', 'shipment'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// UPDATE INVOICE
|
||||
// UPDATE INVOICE (HEADER LEVEL)
|
||||
// -------------------------------------------------------------
|
||||
// -------------------------------------------------------------
|
||||
// UPDATE INVOICE (HEADER LEVEL)
|
||||
// -------------------------------------------------------------
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
Log::info("🟡 Invoice Update Request Received", [
|
||||
Log::info('🟡 Invoice Update Request Received', [
|
||||
'invoice_id' => $id,
|
||||
'request' => $request->all()
|
||||
'request' => $request->all(),
|
||||
]);
|
||||
|
||||
$invoice = Invoice::findOrFail($id);
|
||||
|
||||
// 1) VALIDATION
|
||||
$data = $request->validate([
|
||||
'invoice_date' => 'required|date',
|
||||
'due_date' => 'required|date|after_or_equal:invoice_date',
|
||||
@@ -70,31 +72,32 @@ class AdminInvoiceController extends Controller
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
|
||||
Log::info("✅ Validated Invoice Update Data", $data);
|
||||
Log::info('✅ Validated Invoice Update Data', $data);
|
||||
|
||||
$finalAmount = floatval($data['final_amount']);
|
||||
$taxPercent = floatval($data['tax_percent']);
|
||||
$taxAmount = 0;
|
||||
// 2) CALCULATE GST / TOTALS
|
||||
$finalAmount = (float) $data['final_amount'];
|
||||
$taxPercent = (float) $data['tax_percent'];
|
||||
|
||||
if ($data['tax_type'] === 'gst') {
|
||||
Log::info("🟢 GST Selected", compact('taxPercent'));
|
||||
Log::info('🟢 GST Selected', compact('taxPercent'));
|
||||
|
||||
$data['cgst_percent'] = $taxPercent / 2;
|
||||
$data['sgst_percent'] = $taxPercent / 2;
|
||||
$data['igst_percent'] = 0;
|
||||
} else {
|
||||
Log::info("🔵 IGST Selected", compact('taxPercent'));
|
||||
Log::info('🔵 IGST Selected', compact('taxPercent'));
|
||||
|
||||
$data['cgst_percent'] = 0;
|
||||
$data['sgst_percent'] = 0;
|
||||
$data['igst_percent'] = $taxPercent;
|
||||
}
|
||||
|
||||
$taxAmount = ($finalAmount * $taxPercent) / 100;
|
||||
|
||||
$data['gst_amount'] = $taxAmount;
|
||||
$data['final_amount_with_gst'] = $finalAmount + $taxAmount;
|
||||
$gstAmount = ($finalAmount * $taxPercent) / 100;
|
||||
$data['gst_amount'] = $gstAmount;
|
||||
$data['final_amount_with_gst'] = $finalAmount + $gstAmount;
|
||||
$data['gst_percent'] = $taxPercent;
|
||||
|
||||
Log::info("📌 Final Calculated Invoice Values", [
|
||||
Log::info('📌 Final Calculated Invoice Values', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'final_amount' => $finalAmount,
|
||||
'gst_amount' => $data['gst_amount'],
|
||||
@@ -105,13 +108,28 @@ class AdminInvoiceController extends Controller
|
||||
'igst_percent' => $data['igst_percent'],
|
||||
]);
|
||||
|
||||
// 3) UPDATE DB
|
||||
$invoice->update($data);
|
||||
|
||||
Log::info("✅ Invoice Updated Successfully", [
|
||||
'invoice_id' => $invoice->id
|
||||
Log::info('✅ Invoice Updated Successfully', [
|
||||
'invoice_id' => $invoice->id,
|
||||
]);
|
||||
|
||||
// regenerate PDF
|
||||
// 4) LOG ACTUAL DB VALUES
|
||||
$invoice->refresh();
|
||||
Log::info('🔍 Invoice AFTER UPDATE (DB values)', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'final_amount' => $invoice->final_amount,
|
||||
'gst_percent' => $invoice->gst_percent,
|
||||
'gst_amount' => $invoice->gst_amount,
|
||||
'final_amount_with_gst' => $invoice->final_amount_with_gst,
|
||||
'tax_type' => $invoice->tax_type,
|
||||
'cgst_percent' => $invoice->cgst_percent,
|
||||
'sgst_percent' => $invoice->sgst_percent,
|
||||
'igst_percent' => $invoice->igst_percent,
|
||||
]);
|
||||
|
||||
// 5) REGENERATE PDF
|
||||
$this->generateInvoicePDF($invoice);
|
||||
|
||||
return redirect()
|
||||
@@ -119,13 +137,89 @@ class AdminInvoiceController extends Controller
|
||||
->with('success', 'Invoice updated & PDF generated successfully.');
|
||||
}
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 🔹 UPDATE INVOICE ITEMS (price + ttl_amount)
|
||||
// -------------------------------------------------------------
|
||||
public function updateItems(Request $request, Invoice $invoice)
|
||||
{
|
||||
Log::info('🟡 Invoice Items Update Request', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'payload' => $request->all(),
|
||||
]);
|
||||
|
||||
$data = $request->validate([
|
||||
'items' => ['required', 'array'],
|
||||
'items.*.price' => ['required', 'numeric', 'min:0'],
|
||||
'items.*.ttl_amount' => ['required', 'numeric', 'min:0'],
|
||||
]);
|
||||
|
||||
$itemsInput = $data['items'];
|
||||
|
||||
foreach ($itemsInput as $itemId => $itemData) {
|
||||
$item = InvoiceItem::where('id', $itemId)
|
||||
->where('invoice_id', $invoice->id)
|
||||
->first();
|
||||
|
||||
if (!$item) {
|
||||
Log::warning('Invoice item not found or mismatched invoice', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'item_id' => $itemId,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$item->price = $itemData['price'];
|
||||
$item->ttl_amount = $itemData['ttl_amount'];
|
||||
$item->save();
|
||||
}
|
||||
|
||||
$newBaseAmount = InvoiceItem::where('invoice_id', $invoice->id)
|
||||
->sum('ttl_amount');
|
||||
|
||||
$taxType = $invoice->tax_type;
|
||||
$cgstPercent = (float) ($invoice->cgst_percent ?? 0);
|
||||
$sgstPercent = (float) ($invoice->sgst_percent ?? 0);
|
||||
$igstPercent = (float) ($invoice->igst_percent ?? 0);
|
||||
|
||||
$gstPercent = 0;
|
||||
if ($taxType === 'gst') {
|
||||
$gstPercent = $cgstPercent + $sgstPercent;
|
||||
} elseif ($taxType === 'igst') {
|
||||
$gstPercent = $igstPercent;
|
||||
}
|
||||
|
||||
$gstAmount = $newBaseAmount * $gstPercent / 100;
|
||||
$finalWithGst = $newBaseAmount + $gstAmount;
|
||||
|
||||
$invoice->final_amount = $newBaseAmount;
|
||||
$invoice->gst_amount = $gstAmount;
|
||||
$invoice->final_amount_with_gst = $finalWithGst;
|
||||
$invoice->gst_percent = $gstPercent;
|
||||
$invoice->save();
|
||||
|
||||
Log::info('✅ Invoice items updated & totals recalculated', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'final_amount' => $invoice->final_amount,
|
||||
'gst_amount' => $invoice->gst_amount,
|
||||
'final_amount_with_gst' => $invoice->final_amount_with_gst,
|
||||
'tax_type' => $invoice->tax_type,
|
||||
'cgst_percent' => $invoice->cgst_percent,
|
||||
'sgst_percent' => $invoice->sgst_percent,
|
||||
'igst_percent' => $invoice->igst_percent,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Invoice items updated successfully.');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PDF GENERATION USING mPDF
|
||||
// -------------------------------------------------------------
|
||||
public function generateInvoicePDF($invoice)
|
||||
{
|
||||
$invoice->load(['items', 'order.shipments']);
|
||||
$shipment = $invoice->order?->shipments?->first();
|
||||
$invoice->load(['items', 'customer', 'container']);
|
||||
$shipment = null;
|
||||
|
||||
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
|
||||
$folder = public_path('invoices/');
|
||||
|
||||
@@ -134,19 +228,30 @@ class AdminInvoiceController extends Controller
|
||||
}
|
||||
|
||||
$filePath = $folder . $fileName;
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
$mpdf = new Mpdf(['mode' => 'utf-8', 'format' => 'A4', 'default_font' => 'sans-serif']);
|
||||
$html = view('admin.pdf.invoice', ['invoice' => $invoice, 'shipment' => $shipment])->render();
|
||||
$mpdf = new Mpdf([
|
||||
'mode' => 'utf-8',
|
||||
'format' => 'A4',
|
||||
'default_font' => 'sans-serif',
|
||||
]);
|
||||
|
||||
$html = view('admin.pdf.invoice', [
|
||||
'invoice' => $invoice,
|
||||
'shipment' => $shipment,
|
||||
])->render();
|
||||
|
||||
$mpdf->WriteHTML($html);
|
||||
$mpdf->Output($filePath, 'F');
|
||||
|
||||
$invoice->update(['pdf_path' => 'invoices/' . $fileName]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INSTALLMENTS (ADD/DELETE)
|
||||
// INSTALLMENTS (ADD)
|
||||
// -------------------------------------------------------------
|
||||
public function storeInstallment(Request $request, $invoice_id)
|
||||
{
|
||||
@@ -158,15 +263,13 @@ class AdminInvoiceController extends Controller
|
||||
]);
|
||||
|
||||
$invoice = Invoice::findOrFail($invoice_id);
|
||||
|
||||
$paidTotal = $invoice->installments()->sum('amount');
|
||||
// Use GST-inclusive total for all calculations/checks
|
||||
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
||||
|
||||
if ($request->amount > $remaining) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Installment amount exceeds remaining balance.'
|
||||
'message' => 'Installment amount exceeds remaining balance.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
@@ -180,7 +283,6 @@ class AdminInvoiceController extends Controller
|
||||
|
||||
$newPaid = $paidTotal + $request->amount;
|
||||
|
||||
// Mark as 'paid' if GST-inclusive total is cleared
|
||||
if ($newPaid >= $invoice->final_amount_with_gst) {
|
||||
$invoice->update(['status' => 'paid']);
|
||||
}
|
||||
@@ -194,9 +296,13 @@ class AdminInvoiceController extends Controller
|
||||
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
||||
'baseAmount' => $invoice->final_amount,
|
||||
'remaining' => max(0, $invoice->final_amount_with_gst - $newPaid),
|
||||
'isCompleted' => $newPaid >= $invoice->final_amount_with_gst
|
||||
'isCompleted' => $newPaid >= $invoice->final_amount_with_gst,
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INSTALLMENTS (DELETE)
|
||||
// -------------------------------------------------------------
|
||||
public function deleteInstallment($id)
|
||||
{
|
||||
$installment = InvoiceInstallment::findOrFail($id);
|
||||
@@ -207,8 +313,7 @@ class AdminInvoiceController extends Controller
|
||||
$paidTotal = $invoice->installments()->sum('amount');
|
||||
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
||||
|
||||
// Update status if not fully paid anymore
|
||||
if ($remaining > 0 && $invoice->status === "paid") {
|
||||
if ($remaining > 0 && $invoice->status === 'paid') {
|
||||
$invoice->update(['status' => 'pending']);
|
||||
}
|
||||
|
||||
@@ -220,8 +325,7 @@ class AdminInvoiceController extends Controller
|
||||
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
||||
'baseAmount' => $invoice->final_amount,
|
||||
'remaining' => $remaining,
|
||||
'isZero' => $paidTotal == 0
|
||||
'isZero' => $paidTotal == 0,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ class AdminOrderController extends Controller
|
||||
]);
|
||||
|
||||
//If you want to auto-create an invoice at order creation, uncomment:
|
||||
$this->createInvoice($order);
|
||||
// $this->createInvoice($order);
|
||||
|
||||
return redirect()->route('admin.orders.show', $order->id)
|
||||
->with('success', 'Order created successfully.');
|
||||
@@ -148,7 +148,7 @@ class AdminOrderController extends Controller
|
||||
|
||||
// recalc totals and save to order
|
||||
$this->recalcTotals($order);
|
||||
$this->updateInvoiceFromOrder($order); // <-- NEW
|
||||
// $this->updateInvoiceFromOrder($order); // <-- NEW
|
||||
|
||||
return redirect()->back()->with('success', 'Item added and totals updated.');
|
||||
}
|
||||
@@ -594,70 +594,70 @@ class AdminOrderController extends Controller
|
||||
// =======================
|
||||
|
||||
// 1. Auto-generate invoice number
|
||||
$lastInvoice = \App\Models\Invoice::latest()->first();
|
||||
$nextInvoice = $lastInvoice ? $lastInvoice->id + 1 : 1;
|
||||
$invoiceNumber = 'INV-' . date('Y') . '-' . str_pad($nextInvoice, 6, '0', STR_PAD_LEFT);
|
||||
// $lastInvoice = \App\Models\Invoice::latest()->first();
|
||||
// $nextInvoice = $lastInvoice ? $lastInvoice->id + 1 : 1;
|
||||
// $invoiceNumber = 'INV-' . date('Y') . '-' . str_pad($nextInvoice, 6, '0', STR_PAD_LEFT);
|
||||
|
||||
// 2. Fetch customer (using mark list → customer_id)
|
||||
$markList = MarkList::where('mark_no', $order->mark_no)->first();
|
||||
$customer = null;
|
||||
// $markList = MarkList::where('mark_no', $order->mark_no)->first();
|
||||
// $customer = null;
|
||||
|
||||
if ($markList && $markList->customer_id) {
|
||||
$customer = \App\Models\User::where('customer_id', $markList->customer_id)->first();
|
||||
}
|
||||
// if ($markList && $markList->customer_id) {
|
||||
// $customer = \App\Models\User::where('customer_id', $markList->customer_id)->first();
|
||||
// }
|
||||
|
||||
// 3. Create Invoice Record
|
||||
$invoice = \App\Models\Invoice::create([
|
||||
'order_id' => $order->id,
|
||||
'customer_id' => $customer->id ?? null,
|
||||
'mark_no' => $order->mark_no,
|
||||
// $invoice = \App\Models\Invoice::create([
|
||||
// 'order_id' => $order->id,
|
||||
// 'customer_id' => $customer->id ?? null,
|
||||
// 'mark_no' => $order->mark_no,
|
||||
|
||||
'invoice_number' => $invoiceNumber,
|
||||
'invoice_date' => now(),
|
||||
'due_date' => now()->addDays(10),
|
||||
// 'invoice_number' => $invoiceNumber,
|
||||
// 'invoice_date' => now(),
|
||||
// 'due_date' => now()->addDays(10),
|
||||
|
||||
'payment_method' => null,
|
||||
'reference_no' => null,
|
||||
'status' => 'pending',
|
||||
// 'payment_method' => null,
|
||||
// 'reference_no' => null,
|
||||
// 'status' => 'pending',
|
||||
|
||||
'final_amount' => $total_amount,
|
||||
'gst_percent' => 0,
|
||||
'gst_amount' => 0,
|
||||
'final_amount_with_gst' => $total_amount,
|
||||
// 'final_amount' => $total_amount,
|
||||
// 'gst_percent' => 0,
|
||||
// 'gst_amount' => 0,
|
||||
// 'final_amount_with_gst' => $total_amount,
|
||||
|
||||
// snapshot customer fields
|
||||
'customer_name' => $customer->customer_name ?? null,
|
||||
'company_name' => $customer->company_name ?? null,
|
||||
'customer_email' => $customer->email ?? null,
|
||||
'customer_mobile' => $customer->mobile_no ?? null,
|
||||
'customer_address' => $customer->address ?? null,
|
||||
'pincode' => $customer->pincode ?? null,
|
||||
// // snapshot customer fields
|
||||
// 'customer_name' => $customer->customer_name ?? null,
|
||||
// 'company_name' => $customer->company_name ?? null,
|
||||
// 'customer_email' => $customer->email ?? null,
|
||||
// 'customer_mobile' => $customer->mobile_no ?? null,
|
||||
// 'customer_address' => $customer->address ?? null,
|
||||
// 'pincode' => $customer->pincode ?? null,
|
||||
|
||||
'notes' => null,
|
||||
]);
|
||||
// 'notes' => null,
|
||||
// ]);
|
||||
|
||||
// 4. Clone order items into invoice_items
|
||||
foreach ($order->items as $item) {
|
||||
\App\Models\InvoiceItem::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
'description' => $item->description,
|
||||
'ctn' => $item->ctn,
|
||||
'qty' => $item->qty,
|
||||
'ttl_qty' => $item->ttl_qty,
|
||||
'unit' => $item->unit,
|
||||
'price' => $item->price,
|
||||
'ttl_amount' => $item->ttl_amount,
|
||||
'cbm' => $item->cbm,
|
||||
'ttl_cbm' => $item->ttl_cbm,
|
||||
'kg' => $item->kg,
|
||||
'ttl_kg' => $item->ttl_kg,
|
||||
'shop_no' => $item->shop_no,
|
||||
]);
|
||||
}
|
||||
// foreach ($order->items as $item) {
|
||||
// \App\Models\InvoiceItem::create([
|
||||
// 'invoice_id' => $invoice->id,
|
||||
// 'description' => $item->description,
|
||||
// 'ctn' => $item->ctn,
|
||||
// 'qty' => $item->qty,
|
||||
// 'ttl_qty' => $item->ttl_qty,
|
||||
// 'unit' => $item->unit,
|
||||
// 'price' => $item->price,
|
||||
// 'ttl_amount' => $item->ttl_amount,
|
||||
// 'cbm' => $item->cbm,
|
||||
// 'ttl_cbm' => $item->ttl_cbm,
|
||||
// 'kg' => $item->kg,
|
||||
// 'ttl_kg' => $item->ttl_kg,
|
||||
// 'shop_no' => $item->shop_no,
|
||||
// ]);
|
||||
// }
|
||||
|
||||
// 5. TODO: PDF generation (I will add this later)
|
||||
$invoice->pdf_path = null; // placeholder for now
|
||||
$invoice->save();
|
||||
// $invoice->pdf_path = null; // placeholder for now
|
||||
// $invoice->save();
|
||||
|
||||
// =======================
|
||||
// END INVOICE CREATION
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
93
app/Http/Controllers/user/ChatController.php
Normal file
93
app/Http/Controllers/user/ChatController.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\SupportTicket;
|
||||
use App\Models\ChatMessage;
|
||||
use App\Events\NewChatMessage;
|
||||
|
||||
class ChatController extends Controller
|
||||
{
|
||||
/**
|
||||
* Start chat or return existing ticket for this user
|
||||
*/
|
||||
public function startChat()
|
||||
{
|
||||
// One chat ticket per user
|
||||
$ticket = SupportTicket::firstOrCreate([
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'ticket' => $ticket
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all messages for this ticket
|
||||
*/
|
||||
public function getMessages($ticketId)
|
||||
{
|
||||
// Ensure this ticket belongs to the logged-in user
|
||||
$ticket = SupportTicket::where('id', $ticketId)
|
||||
->where('user_id', auth()->id())
|
||||
->firstOrFail();
|
||||
|
||||
$messages = ChatMessage::where('ticket_id', $ticketId)
|
||||
->orderBy('created_at', 'asc')
|
||||
->with('sender')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'messages' => $messages
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send text or file message from user → admin/staff
|
||||
*/
|
||||
public function sendMessage(Request $request, $ticketId)
|
||||
{
|
||||
$request->validate([
|
||||
'message' => 'nullable|string',
|
||||
'file' => 'nullable|file|max:20480', // 20MB limit
|
||||
]);
|
||||
|
||||
// Validate ticket ownership
|
||||
$ticket = SupportTicket::where('id', $ticketId)
|
||||
->where('user_id', auth()->id())
|
||||
->firstOrFail();
|
||||
|
||||
$data = [
|
||||
'ticket_id' => $ticketId,
|
||||
'sender_id' => auth()->id(),
|
||||
'sender_type' => \App\Models\User::class,
|
||||
'message' => $request->message,
|
||||
];
|
||||
|
||||
// Handle file upload
|
||||
if ($request->hasFile('file')) {
|
||||
$path = $request->file('file')->store('chat', 'public');
|
||||
$data['file_path'] = $path;
|
||||
$data['file_type'] = $request->file('file')->getMimeType();
|
||||
}
|
||||
|
||||
// Save message
|
||||
$message = ChatMessage::create($data);
|
||||
|
||||
// Load sender info for broadcast
|
||||
$message->load('sender');
|
||||
|
||||
// Fire real-time event
|
||||
broadcast(new NewChatMessage($message))->toOthers();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
}
|
||||
36
app/Models/ChatMessage.php
Normal file
36
app/Models/ChatMessage.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class ChatMessage extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'ticket_id',
|
||||
'sender_id',
|
||||
'sender_type', // user OR admin
|
||||
'message',
|
||||
'file_path',
|
||||
'file_type',
|
||||
];
|
||||
|
||||
/**
|
||||
* The ticket this message belongs to.
|
||||
*/
|
||||
public function ticket()
|
||||
{
|
||||
return $this->belongsTo(SupportTicket::class, 'ticket_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Polymorphic sender (User or Admin)
|
||||
*/
|
||||
public function sender()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
30
app/Models/Container.php
Normal file
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,15 +83,10 @@ class Invoice extends Model
|
||||
return $this->status === 'pending' && now()->gt($this->due_date);
|
||||
}
|
||||
|
||||
// जर पुढे container → shipment relation असेल तर हा helper नंतर adjust करू
|
||||
public function getShipment()
|
||||
{
|
||||
return $this->order?->shipments?->first();
|
||||
// आधी order वरून shipment घेत होत; container flow मध्ये नंतर गरज पडल्यास बदलू
|
||||
return null;
|
||||
}
|
||||
|
||||
public function installments()
|
||||
{
|
||||
return $this->hasMany(InvoiceInstallment::class);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
32
app/Models/SupportTicket.php
Normal file
32
app/Models/SupportTicket.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class SupportTicket extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'status',
|
||||
];
|
||||
|
||||
/**
|
||||
* The user (customer) who owns this ticket.
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* All chat messages for this ticket.
|
||||
*/
|
||||
public function messages()
|
||||
{
|
||||
return $this->hasMany(ChatMessage::class, 'ticket_id')->orderBy('created_at', 'asc');
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,9 @@
|
||||
"php": "^8.2",
|
||||
"barryvdh/laravel-dompdf": "^3.1",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/reverb": "^1.6",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"maatwebsite/excel": "^1.1",
|
||||
"maatwebsite/excel": "^3.1",
|
||||
"mpdf/mpdf": "^8.2",
|
||||
"php-open-source-saver/jwt-auth": "2.8",
|
||||
"spatie/laravel-permission": "^6.23"
|
||||
@@ -84,7 +85,8 @@
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"platform-check": false
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
|
||||
1563
composer.lock
generated
1563
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
31
config/broadcasting.php
Normal file
31
config/broadcasting.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'default' => env('BROADCAST_DRIVER', 'null'),
|
||||
|
||||
'connections' => [
|
||||
|
||||
'reverb' => [
|
||||
'driver' => 'reverb',
|
||||
'key' => env('REVERB_APP_KEY'),
|
||||
'secret' => env('REVERB_APP_SECRET'),
|
||||
'app_id' => env('REVERB_APP_ID'),
|
||||
'options' => [
|
||||
'host' => env('REVERB_HOST'),
|
||||
'port' => env('REVERB_PORT'),
|
||||
'scheme' => env('REVERB_SCHEME'),
|
||||
'useTLS' => false,
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
'log' => [
|
||||
'driver' => 'log',
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'null',
|
||||
],
|
||||
],
|
||||
];
|
||||
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,
|
||||
],
|
||||
];
|
||||
96
config/reverb.php
Normal file
96
config/reverb.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Reverb Server
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'default' => env('REVERB_SERVER', 'reverb'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reverb Servers
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'servers' => [
|
||||
|
||||
'reverb' => [
|
||||
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'), // WebSocket listens here
|
||||
'port' => env('REVERB_SERVER_PORT', 8080), // WebSocket port
|
||||
'path' => env('REVERB_SERVER_PATH', ''),
|
||||
|
||||
// Used for Echo client hostname
|
||||
'hostname' => env('REVERB_HOST', 'localhost'),
|
||||
|
||||
'options' => [
|
||||
'tls' => [], // No TLS for localhost
|
||||
],
|
||||
|
||||
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10000),
|
||||
|
||||
'scaling' => [
|
||||
'enabled' => env('REVERB_SCALING_ENABLED', false),
|
||||
],
|
||||
|
||||
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
|
||||
'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reverb Applications
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'apps' => [
|
||||
|
||||
'provider' => 'config',
|
||||
|
||||
'apps' => [
|
||||
[
|
||||
'key' => env('REVERB_APP_KEY'),
|
||||
'secret' => env('REVERB_APP_SECRET'),
|
||||
'app_id' => env('REVERB_APP_ID'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Echo + Flutter Client Options
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'options' => [
|
||||
'host' => env('REVERB_HOST', 'localhost'), // for client connections
|
||||
'port' => env('REVERB_PORT', 8080), // SAME as WebSocket server port
|
||||
'scheme' => env('REVERB_SCHEME', 'http'),
|
||||
'useTLS' => false,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Allowed Origins (Important)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| "*" allows all origins:
|
||||
| - Flutter (Android/iOS/Web)
|
||||
| - Admin Panel
|
||||
| - Localhost
|
||||
|
|
||||
*/
|
||||
'allowed_origins' => ['*'],
|
||||
|
||||
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
|
||||
'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
|
||||
'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'),
|
||||
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10000),
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -13,16 +13,28 @@ return new class extends Migration
|
||||
{
|
||||
Schema::create('chat_messages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('ticket_id'); // support ticket ID
|
||||
$table->unsignedBigInteger('sender_id'); // user or admin/staff
|
||||
$table->text('message')->nullable(); // message content
|
||||
$table->string('file_path')->nullable(); // image/pdf/video
|
||||
$table->string('file_type')->default('text'); // text/image/pdf/video
|
||||
|
||||
// Chat belongs to a ticket
|
||||
$table->unsignedBigInteger('ticket_id');
|
||||
|
||||
// POLYMORPHIC sender (User OR Admin)
|
||||
$table->unsignedBigInteger('sender_id');
|
||||
$table->string('sender_type');
|
||||
// Example values:
|
||||
// - "App\Models\User"
|
||||
// - "App\Models\Admin"
|
||||
|
||||
// Content
|
||||
$table->text('message')->nullable();
|
||||
$table->string('file_path')->nullable(); // storage/app/public/chat/...
|
||||
$table->string('file_type')->default('text'); // text / image / video / pdf
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// foreign keys
|
||||
$table->foreign('ticket_id')->references('id')->on('support_tickets')->onDelete('cascade');
|
||||
$table->foreign('sender_id')->references('id')->on('users')->onDelete('cascade'); // admin also stored in users table? If admin separate, change later.
|
||||
// FK to tickets table
|
||||
$table->foreign('ticket_id')
|
||||
->references('id')->on('support_tickets')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,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');
|
||||
});
|
||||
}
|
||||
};
|
||||
2525
package-lock.json
generated
Normal file
2525
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,5 +13,9 @@
|
||||
"laravel-vite-plugin": "^2.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^7.0.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"laravel-echo": "^2.2.6",
|
||||
"pusher-js": "^8.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
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 +1,6 @@
|
||||
import './bootstrap';
|
||||
import "./bootstrap";
|
||||
|
||||
// VERY IMPORTANT — Load Echo globally
|
||||
import "./echo";
|
||||
|
||||
console.log("[APP] app.js loaded");
|
||||
|
||||
31
resources/js/echo.js
Normal file
31
resources/js/echo.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import Echo from 'laravel-echo';
|
||||
import Pusher from 'pusher-js';
|
||||
|
||||
window.Pusher = Pusher;
|
||||
|
||||
// Get CSRF token from meta tag
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
window.Echo = new Echo({
|
||||
broadcaster: 'reverb',
|
||||
key: import.meta.env.VITE_REVERB_APP_KEY,
|
||||
wsHost: import.meta.env.VITE_REVERB_HOST,
|
||||
wsPort: import.meta.env.VITE_REVERB_PORT ?? 8080,
|
||||
wssPort: import.meta.env.VITE_REVERB_PORT ?? 8080,
|
||||
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'http') === 'https',
|
||||
enabledTransports: ['ws', 'wss'],
|
||||
|
||||
authEndpoint: '/admin/broadcasting/auth',
|
||||
|
||||
|
||||
// ⭐ MOST IMPORTANT ⭐
|
||||
withCredentials: true,
|
||||
|
||||
auth: {
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('%c[ECHO] Initialized!', 'color: green; font-weight: bold;', window.Echo);
|
||||
@@ -1,12 +1,94 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Dashboard')
|
||||
@section('page-title', 'Chat Support')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="container py-4">
|
||||
|
||||
<h2 class="mb-4 fw-bold">Customer Support Chat</h2>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h4>Welcome to the Admin chat</h4>
|
||||
<p>Here you can manage all system modules.</p>
|
||||
<div class="card-body p-0">
|
||||
|
||||
@if($tickets->count() === 0)
|
||||
<div class="p-4 text-center text-muted">
|
||||
<h5>No customer chats yet.</h5>
|
||||
</div>
|
||||
@else
|
||||
<ul class="list-group list-group-flush">
|
||||
|
||||
@foreach($tickets as $ticket)
|
||||
@php
|
||||
// Get last message
|
||||
$lastMsg = $ticket->messages()->latest()->first();
|
||||
@endphp
|
||||
|
||||
<li class="list-group-item py-3">
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
|
||||
<!-- Left side: User info + last message -->
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
|
||||
<!-- Profile Circle -->
|
||||
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center"
|
||||
style="width: 45px; height: 45px; font-size: 18px;">
|
||||
{{ strtoupper(substr($ticket->user->customer_name ?? $ticket->user->name, 0, 1)) }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- Customer Name -->
|
||||
<h6 class="mb-1 fw-semibold">
|
||||
{{ $ticket->user->customer_name ?? $ticket->user->name }}
|
||||
</h6>
|
||||
|
||||
<!-- Last message preview -->
|
||||
<small class="text-muted">
|
||||
@if($lastMsg)
|
||||
@if($lastMsg->message)
|
||||
{{ Str::limit($lastMsg->message, 35) }}
|
||||
@elseif($lastMsg->file_type === 'image')
|
||||
📷 Image
|
||||
@elseif($lastMsg->file_type === 'video')
|
||||
🎥 Video
|
||||
@else
|
||||
📎 Attachment
|
||||
@endif
|
||||
@else
|
||||
<i>No messages yet</i>
|
||||
@endif
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Right Side: Status + Button -->
|
||||
<div class="text-end">
|
||||
|
||||
<!-- Ticket Status -->
|
||||
<span class="badge
|
||||
{{ $ticket->status === 'open' ? 'bg-success' : 'bg-danger' }}">
|
||||
{{ ucfirst($ticket->status) }}
|
||||
</span>
|
||||
|
||||
<!-- Open Chat Button -->
|
||||
<a href="{{ route('admin.chat.open', $ticket->id) }}"
|
||||
class="btn btn-sm btn-primary ms-2">
|
||||
Open Chat →
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
@endforeach
|
||||
|
||||
</ul>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
201
resources/views/admin/chat_window.blade.php
Normal file
201
resources/views/admin/chat_window.blade.php
Normal file
@@ -0,0 +1,201 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Chat With ' . ($ticket->user->customer_name ?? $ticket->user->name))
|
||||
|
||||
@section('content')
|
||||
|
||||
<style>
|
||||
.chat-box {
|
||||
height: 70vh;
|
||||
overflow-y: auto;
|
||||
background: #f5f6fa;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
.message {
|
||||
max-width: 65%;
|
||||
padding: 10px 14px;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.message.admin {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.message.user {
|
||||
background: #ffffff;
|
||||
border: 1px solid #ddd;
|
||||
margin-right: auto;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.chat-input {
|
||||
position: fixed;
|
||||
bottom: 15px;
|
||||
left: 250px;
|
||||
right: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container py-4">
|
||||
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<h4 class="fw-bold mb-0">
|
||||
Chat With: {{ $ticket->user->customer_name ?? $ticket->user->name }}
|
||||
</h4>
|
||||
<span class="badge ms-3 {{ $ticket->status === 'open' ? 'bg-success' : 'bg-danger' }}">
|
||||
{{ ucfirst($ticket->status) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id="chatBox" class="chat-box border shadow-sm">
|
||||
|
||||
@foreach($messages as $msg)
|
||||
<div class="message {{ $msg->sender_type === 'App\\Models\\Admin' ? 'admin' : 'user' }}">
|
||||
|
||||
{{-- TEXT --}}
|
||||
@if($msg->message)
|
||||
<div>{{ $msg->message }}</div>
|
||||
@endif
|
||||
|
||||
{{-- FILE --}}
|
||||
@if($msg->file_path)
|
||||
<div class="mt-2">
|
||||
@php $isImage = Str::startsWith($msg->file_type, 'image'); @endphp
|
||||
|
||||
@if($isImage)
|
||||
<img src="{{ asset('storage/'.$msg->file_path) }}" style="max-width:150px;" class="rounded">
|
||||
@else
|
||||
<a href="{{ asset('storage/'.$msg->file_path) }}" target="_blank">📎 View Attachment</a>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<small class="text-muted d-block mt-1">
|
||||
{{ $msg->created_at->format('d M h:i A') }}
|
||||
</small>
|
||||
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
|
||||
<div class="chat-input">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body d-flex align-items-center gap-2">
|
||||
<input type="text" id="messageInput" class="form-control" placeholder="Type your message...">
|
||||
<input type="file" id="fileInput" class="form-control" style="max-width:200px;">
|
||||
<button class="btn btn-primary" id="sendBtn">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
console.log("CHAT WINDOW: script loaded");
|
||||
|
||||
// -------------------------------
|
||||
// WAIT FOR ECHO READY
|
||||
// -------------------------------
|
||||
function waitForEcho(callback, retries = 40) {
|
||||
if (window.Echo) {
|
||||
console.log("%c[ECHO] Ready!", "color: green; font-weight: bold;", window.Echo);
|
||||
return callback();
|
||||
}
|
||||
|
||||
console.warn("[ECHO] Not ready. Retrying...");
|
||||
if (retries <= 0) {
|
||||
console.error("[ECHO] FAILED to initialize after retry limit");
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => waitForEcho(callback, retries - 1), 200);
|
||||
}
|
||||
|
||||
// Scroll chat down
|
||||
function scrollToBottom() {
|
||||
const el = document.getElementById("chatBox");
|
||||
if (el) el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
scrollToBottom();
|
||||
|
||||
// -------------------------------
|
||||
// SEND MESSAGE (WORKING PART FROM SCRIPT #1)
|
||||
// -------------------------------
|
||||
document.getElementById("sendBtn").addEventListener("click", function () {
|
||||
console.log("[SEND] Attempting to send message...");
|
||||
|
||||
let msg = document.getElementById("messageInput").value;
|
||||
let file = document.getElementById("fileInput").files[0];
|
||||
|
||||
if (!msg.trim() && !file) {
|
||||
alert("Please type something or upload a file.");
|
||||
return;
|
||||
}
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append("message", msg);
|
||||
if (file) formData.append("file", file);
|
||||
|
||||
fetch("{{ route('admin.chat.send', $ticket->id) }}", {
|
||||
method: "POST",
|
||||
headers: { "X-CSRF-TOKEN": "{{ csrf_token() }}" },
|
||||
body: formData
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then((response) => {
|
||||
console.log("[SEND] Message sent:", response);
|
||||
document.getElementById("messageInput").value = "";
|
||||
document.getElementById("fileInput").value = "";
|
||||
})
|
||||
.catch(err => console.error("[SEND] Error:", err));
|
||||
});
|
||||
|
||||
// -------------------------------
|
||||
// LISTEN FOR REALTIME MESSAGE (WORKING PART FROM SCRIPT #2)
|
||||
// -------------------------------
|
||||
waitForEcho(() => {
|
||||
const ticketId = "{{ $ticket->id }}";
|
||||
|
||||
console.log("[ECHO] Subscribing to channel:", `ticket.${ticketId}`);
|
||||
|
||||
window.Echo.private(`ticket.${ticketId}`)
|
||||
.listen("NewChatMessage", (event) => {
|
||||
|
||||
console.log("%c[REALTIME RECEIVED]", "color: blue; font-weight: bold;", event);
|
||||
|
||||
const msg = event.message;
|
||||
|
||||
let html = `
|
||||
<div class="message ${msg.sender_type === 'App\\Models\\Admin' ? 'admin' : 'user'}">
|
||||
${msg.message ?? ''}
|
||||
`;
|
||||
|
||||
if (msg.file_url) {
|
||||
if (msg.file_type.startsWith("image")) {
|
||||
html += `<img src="${msg.file_url}" class="rounded mt-2" style="max-width:150px;">`;
|
||||
} else {
|
||||
html += `<a href="${msg.file_url}" target="_blank" class="mt-2 d-block">📎 View File</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += `
|
||||
<small class="text-muted d-block mt-1">Just now</small>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById("chatBox").innerHTML += html;
|
||||
scrollToBottom();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
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
|
||||
@@ -4,9 +4,6 @@
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
/* --------------------------------------------------
|
||||
GLOBAL VARIABLES & THEME
|
||||
-------------------------------------------------- */
|
||||
:root {
|
||||
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--success-gradient: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
@@ -20,22 +17,22 @@
|
||||
--border-radius: 12px;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
GLOBAL STYLES & ANIMATIONS
|
||||
-------------------------------------------------- */
|
||||
body {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
|
||||
font-family: "Inter", "Segoe UI", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
@keyframes fadeUp {
|
||||
0% { opacity: 0; transform: translateY(20px) scale(0.98); }
|
||||
100% { opacity: 1; transform: translateY(0) scale(1); }
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.98);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
COMPACT CARD DESIGN
|
||||
-------------------------------------------------- */
|
||||
.glass-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
@@ -52,16 +49,15 @@ body {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(45deg,
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
rgba(102, 126, 234, 0.03),
|
||||
rgba(118, 75, 162, 0.03) 50%,
|
||||
rgba(16, 185, 129, 0.03));
|
||||
rgba(16, 185, 129, 0.03)
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
COMPACT CARD HEADER
|
||||
-------------------------------------------------- */
|
||||
.card-header-compact {
|
||||
background: var(--primary-gradient);
|
||||
color: #fff;
|
||||
@@ -81,9 +77,6 @@ body {
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
COMPACT CARD BODY
|
||||
-------------------------------------------------- */
|
||||
.card-body-compact {
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
@@ -91,9 +84,6 @@ body {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
COMPACT FORM ELEMENTS
|
||||
-------------------------------------------------- */
|
||||
.form-grid-compact {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
@@ -115,25 +105,24 @@ body {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-control-compact, .form-select-compact {
|
||||
.form-control-compact,
|
||||
.form-select-compact {
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
background: #ffffff;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-control-compact:focus, .form-select-compact:focus {
|
||||
.form-control-compact:focus,
|
||||
.form-select-compact:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
COMPACT BUTTONS
|
||||
-------------------------------------------------- */
|
||||
.btn-compact {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
@@ -183,9 +172,6 @@ body {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
COMPACT SUMMARY CARDS
|
||||
-------------------------------------------------- */
|
||||
.summary-grid-compact {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
@@ -194,7 +180,7 @@ body {
|
||||
}
|
||||
|
||||
.summary-card-compact {
|
||||
background: white;
|
||||
background: #ffffff;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-soft);
|
||||
@@ -202,9 +188,17 @@ body {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.summary-card-compact.total { border-left-color: #10b981; }
|
||||
.summary-card-compact.paid { border-left-color: #3b82f6; }
|
||||
.summary-card-compact.remaining { border-left-color: #f59e0b; }
|
||||
.summary-card-compact.total {
|
||||
border-left-color: #10b981;
|
||||
}
|
||||
|
||||
.summary-card-compact.paid {
|
||||
border-left-color: #3b82f6;
|
||||
}
|
||||
|
||||
.summary-card-compact.remaining {
|
||||
border-left-color: #f59e0b;
|
||||
}
|
||||
|
||||
.summary-value-compact {
|
||||
font-size: 1.5rem;
|
||||
@@ -220,11 +214,8 @@ body {
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
COMPACT AMOUNT BREAKDOWN
|
||||
-------------------------------------------------- */
|
||||
.amount-breakdown-compact {
|
||||
background: white;
|
||||
background: #ffffff;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-soft);
|
||||
@@ -254,9 +245,6 @@ body {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
COMPACT TABLE
|
||||
-------------------------------------------------- */
|
||||
.table-compact {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -266,7 +254,7 @@ body {
|
||||
|
||||
.table-compact thead th {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
color: #ffffff;
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
@@ -276,7 +264,7 @@ body {
|
||||
}
|
||||
|
||||
.table-compact tbody tr {
|
||||
background: white;
|
||||
background: #ffffff;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -290,9 +278,6 @@ body {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
BADGE STYLES
|
||||
-------------------------------------------------- */
|
||||
.badge-compact {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
@@ -300,64 +285,53 @@ body {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
RESPONSIVE DESIGN
|
||||
-------------------------------------------------- */
|
||||
@media (max-width: 768px) {
|
||||
.glass-card {
|
||||
margin: 0.5rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.card-body-compact {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.form-grid-compact {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.summary-grid-compact {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-compact {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.table-compact {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Main Container -->
|
||||
<div class="container-fluid py-3">
|
||||
|
||||
<!-- Invoice Preview Section -->
|
||||
{{-- Invoice Preview / Overview --}}
|
||||
<div class="glass-card">
|
||||
<div class="card-header-compact">
|
||||
<h4>
|
||||
<i class="fas fa-file-invoice me-2"></i>Invoice Overview
|
||||
<i class="fas fa-file-invoice me-2"></i>
|
||||
Invoice Overview
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body-compact">
|
||||
@include('admin.popup_invoice', [
|
||||
'invoice' => $invoice,
|
||||
'shipment' => $shipment,
|
||||
'embedded' => true
|
||||
])
|
||||
{{-- इथे popup_invoice मधूनच items table (editable) येईल --}}
|
||||
@include('admin.popup_invoice', ['invoice' => $invoice, 'shipment' => $shipment, 'embedded' => true])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Invoice Form -->
|
||||
{{-- Edit Invoice Header Details (POST) --}}
|
||||
<div class="glass-card">
|
||||
<div class="card-header-compact">
|
||||
<h4>
|
||||
<i class="fas fa-edit me-2"></i>Edit Invoice Details
|
||||
<i class="fas fa-edit me-2"></i>
|
||||
Edit Invoice Details
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body-compact">
|
||||
@@ -365,121 +339,146 @@ body {
|
||||
@csrf
|
||||
|
||||
<div class="form-grid-compact">
|
||||
<!-- Invoice Date -->
|
||||
{{-- Invoice Date --}}
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-calendar-day"></i>
|
||||
Invoice Date
|
||||
<i class="fas fa-calendar-day"></i> Invoice Date
|
||||
</label>
|
||||
<input type="date" class="form-control-compact" name="invoice_date"
|
||||
value="{{ $invoice->invoice_date }}" required>
|
||||
<input type="date"
|
||||
name="invoice_date"
|
||||
class="form-control-compact"
|
||||
value="{{ old('invoice_date', $invoice->invoice_date) }}"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<!-- Due Date -->
|
||||
{{-- Due Date --}}
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-clock"></i>
|
||||
Due Date
|
||||
<i class="fas fa-clock"></i> Due Date
|
||||
</label>
|
||||
<input type="date" class="form-control-compact" name="due_date"
|
||||
value="{{ $invoice->due_date }}" required>
|
||||
<input type="date"
|
||||
name="due_date"
|
||||
class="form-control-compact"
|
||||
value="{{ old('due_date', $invoice->due_date) }}"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<!-- Final Amount -->
|
||||
{{-- Final Amount (Base) --}}
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-money-bill-wave"></i>
|
||||
Final Amount (₹)
|
||||
<i class="fas fa-money-bill-wave"></i> Final Amount (Before GST)
|
||||
</label>
|
||||
<input type="number" step="0.01" class="form-control-compact" name="final_amount"
|
||||
value="{{ $invoice->final_amount }}" required>
|
||||
<input type="number"
|
||||
step="0.01"
|
||||
name="final_amount"
|
||||
class="form-control-compact"
|
||||
value="{{ old('final_amount', $invoice->final_amount) }}"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<!-- Tax Type -->
|
||||
{{-- Tax Type --}}
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-receipt"></i>
|
||||
Tax Type
|
||||
<i class="fas fa-receipt"></i> Tax Type
|
||||
</label>
|
||||
<div class="d-flex gap-3 mt-1">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="tax_type"
|
||||
value="gst" @checked($invoice->tax_type === 'gst')>
|
||||
<label class="form-check-label fw-semibold">GST</label>
|
||||
<input class="form-check-input"
|
||||
type="radio"
|
||||
name="tax_type"
|
||||
value="gst"
|
||||
{{ old('tax_type', $invoice->tax_type) === 'gst' ? 'checked' : '' }}>
|
||||
<label class="form-check-label fw-semibold">GST (CGST + SGST)</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="tax_type"
|
||||
value="igst" @checked($invoice->tax_type === 'igst')>
|
||||
<input class="form-check-input"
|
||||
type="radio"
|
||||
name="tax_type"
|
||||
value="igst"
|
||||
{{ old('tax_type', $invoice->tax_type) === 'igst' ? 'checked' : '' }}>
|
||||
<label class="form-check-label fw-semibold">IGST</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tax Percentage -->
|
||||
{{-- Tax Percentage --}}
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-percentage"></i>
|
||||
Tax Percentage (%)
|
||||
<i class="fas fa-percentage"></i> Tax Percentage
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" max="28" class="form-control-compact"
|
||||
<input type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="28"
|
||||
name="tax_percent"
|
||||
value="{{ old('tax_percent', $invoice->tax_type == 'gst' ? $invoice->cgst_percent + $invoice->sgst_percent : $invoice->igst_percent) }}"
|
||||
class="form-control-compact"
|
||||
value="{{ old('tax_percent',
|
||||
$invoice->tax_type === 'gst'
|
||||
? ($invoice->cgst_percent + $invoice->sgst_percent)
|
||||
: $invoice->igst_percent
|
||||
) }}"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
{{-- Status --}}
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-tasks"></i>
|
||||
Status
|
||||
<i class="fas fa-tasks"></i> Status
|
||||
</label>
|
||||
<select class="form-select-compact" name="status" required>
|
||||
<option value="pending" @selected($invoice->status=='pending')>⏳ Pending</option>
|
||||
<option value="paid" @selected($invoice->status=='paid')>✅ Paid</option>
|
||||
<option value="overdue" @selected($invoice->status=='overdue')>⚠️ Overdue</option>
|
||||
<select name="status" class="form-select-compact" required>
|
||||
<option value="pending" {{ old('status', $invoice->status) === 'pending' ? 'selected' : '' }}>
|
||||
Pending
|
||||
</option>
|
||||
<option value="paid" {{ old('status', $invoice->status) === 'paid' ? 'selected' : '' }}>
|
||||
Paid
|
||||
</option>
|
||||
<option value="overdue" {{ old('status', $invoice->status) === 'overdue' ? 'selected' : '' }}>
|
||||
Overdue
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
{{-- Notes --}}
|
||||
<div class="form-group-compact" style="grid-column: 1 / -1;">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-sticky-note"></i>
|
||||
Notes
|
||||
<i class="fas fa-sticky-note"></i> Notes
|
||||
</label>
|
||||
<textarea class="form-control-compact" rows="3" name="notes"
|
||||
placeholder="Add any additional notes...">{{ $invoice->notes }}</textarea>
|
||||
<textarea name="notes"
|
||||
rows="3"
|
||||
class="form-control-compact"
|
||||
placeholder="Add any additional notes...">{{ old('notes', $invoice->notes) }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-end mt-3">
|
||||
@can('invoice.edit')
|
||||
<button type="submit" class="btn-success-compact btn-compact">
|
||||
<i class="fas fa-save me-2"></i>Update Invoice
|
||||
</button>
|
||||
@endcan
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$totalPaid = $invoice->installments->sum('amount');
|
||||
$totalPaid = $invoice->installments()->sum('amount');
|
||||
$remaining = $invoice->final_amount_with_gst - $totalPaid;
|
||||
@endphp
|
||||
|
||||
<!-- Amount Breakdown -->
|
||||
{{-- Amount Breakdown --}}
|
||||
<div class="amount-breakdown-compact">
|
||||
<h6 class="fw-bold mb-3 text-dark">
|
||||
<i class="fas fa-calculator me-2"></i>Amount Breakdown
|
||||
</h6>
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label">Total Amount (Before Tax):</span>
|
||||
<span class="breakdown-value">₹{{ number_format($invoice->final_amount, 2) }}</span>
|
||||
<span class="breakdown-label">Total Amount Before Tax</span>
|
||||
<span class="breakdown-value" id="baseAmount">
|
||||
₹{{ number_format($invoice->final_amount, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label">Tax Type:</span>
|
||||
<span class="breakdown-label">Tax Type</span>
|
||||
<span class="breakdown-value text-primary">
|
||||
@if($invoice->tax_type === 'gst')
|
||||
GST (CGST + SGST)
|
||||
@@ -490,7 +489,7 @@ body {
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label">Tax Percentage:</span>
|
||||
<span class="breakdown-label">Tax Percentage</span>
|
||||
<span class="breakdown-value text-primary">
|
||||
@if($invoice->tax_type === 'gst')
|
||||
{{ $invoice->cgst_percent + $invoice->sgst_percent }}%
|
||||
@@ -501,114 +500,137 @@ body {
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label">GST Amount:</span>
|
||||
<span class="breakdown-value text-warning">₹{{ number_format($invoice->gst_amount, 2) }}</span>
|
||||
<span class="breakdown-label">GST Amount</span>
|
||||
<span class="breakdown-value text-warning" id="gstAmount">
|
||||
₹{{ number_format($invoice->gst_amount, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row" style="border-top: 2px solid #e2e8f0; padding-top: 0.75rem;">
|
||||
<span class="breakdown-label fw-bold">Total Invoice Amount (Including GST):</span>
|
||||
<span class="breakdown-value fw-bold text-dark">₹{{ number_format($invoice->final_amount_with_gst, 2) }}</span>
|
||||
<span class="breakdown-label fw-bold">Total Invoice Amount Including GST</span>
|
||||
<span class="breakdown-value fw-bold text-dark" id="totalInvoiceWithGst">
|
||||
₹{{ number_format($invoice->final_amount_with_gst, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label text-success">Total Paid:</span>
|
||||
<span class="breakdown-value fw-bold text-success" id="paidAmount">₹{{ number_format($totalPaid, 2) }}</span>
|
||||
<span class="breakdown-label text-success">Total Paid</span>
|
||||
<span class="breakdown-value fw-bold text-success" id="paidAmount">
|
||||
₹{{ number_format($totalPaid, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row" style="border-bottom: none;">
|
||||
<span class="breakdown-label text-danger">Remaining:</span>
|
||||
<span class="breakdown-value fw-bold text-danger" id="remainingAmount">₹{{ number_format($remaining, 2) }}</span>
|
||||
<span class="breakdown-label text-danger">Remaining</span>
|
||||
<span class="breakdown-value fw-bold text-danger" id="remainingAmount">
|
||||
₹{{ number_format($remaining, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Installment Summary -->
|
||||
{{-- Installment Summary --}}
|
||||
<div class="summary-grid-compact">
|
||||
<div class="summary-card-compact total">
|
||||
<div class="summary-value-compact text-success">₹{{ number_format($invoice->final_amount_with_gst, 2) }}</div>
|
||||
<div class="summary-value-compact text-success" id="totalInvoiceWithGstCard">
|
||||
₹{{ number_format($invoice->final_amount_with_gst, 2) }}
|
||||
</div>
|
||||
<div class="summary-label-compact">Total Amount</div>
|
||||
</div>
|
||||
<div class="summary-card-compact paid">
|
||||
<div class="summary-value-compact text-primary">₹{{ number_format($totalPaid, 2) }}</div>
|
||||
<div class="summary-value-compact text-primary">
|
||||
₹{{ number_format($totalPaid, 2) }}
|
||||
</div>
|
||||
<div class="summary-label-compact">Total Paid</div>
|
||||
</div>
|
||||
<div class="summary-card-compact remaining">
|
||||
<div class="summary-value-compact text-warning">₹{{ number_format($remaining, 2) }}</div>
|
||||
<div class="summary-value-compact text-warning">
|
||||
₹{{ number_format($remaining, 2) }}
|
||||
</div>
|
||||
<div class="summary-label-compact">Remaining</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Installment Management -->
|
||||
{{-- Installment Management --}}
|
||||
<div class="glass-card">
|
||||
<div class="card-header-compact d-flex justify-content-between align-items-center">
|
||||
<h4>
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-credit-card me-2"></i>Installment Payments
|
||||
</h4>
|
||||
@can('invoice.add_installment')
|
||||
@if($remaining > 0)
|
||||
<button id="toggleInstallmentForm" class="btn-primary-compact btn-compact">
|
||||
<i class="fas fa-plus-circle me-2"></i>Add Installment
|
||||
</button>
|
||||
@endif
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
<div class="card-body-compact">
|
||||
<!-- Installment Form -->
|
||||
{{-- Installment Form --}}
|
||||
<div id="installmentForm" class="d-none mb-3">
|
||||
<div class="glass-card" style="background: rgba(248, 250, 252, 0.8);">
|
||||
<div class="card-header-compact" style="background: var(--success-gradient);">
|
||||
<h4>
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-plus-circle me-2"></i>Add New Installment
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body-compact">
|
||||
<form id="installmentSubmitForm">
|
||||
@csrf
|
||||
|
||||
<div class="form-grid-compact">
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-calendar-day"></i>
|
||||
Installment Date
|
||||
<i class="fas fa-calendar-day"></i> Installment Date
|
||||
</label>
|
||||
<input type="date" name="installment_date" class="form-control-compact" required>
|
||||
<input type="date"
|
||||
name="installment_date"
|
||||
class="form-control-compact"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-credit-card"></i>
|
||||
Payment Method
|
||||
<i class="fas fa-credit-card"></i> Payment Method
|
||||
</label>
|
||||
<select name="payment_method" class="form-select-compact" required>
|
||||
<option value="cash">💵 Cash</option>
|
||||
<option value="bank">🏦 Bank Transfer</option>
|
||||
<option value="upi">📱 UPI</option>
|
||||
<option value="cheque">🧾 Cheque</option>
|
||||
<select name="payment_method"
|
||||
class="form-select-compact"
|
||||
required>
|
||||
<option value="cash">Cash</option>
|
||||
<option value="bank">Bank Transfer</option>
|
||||
<option value="upi">UPI</option>
|
||||
<option value="cheque">Cheque</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-hashtag"></i>
|
||||
Reference No
|
||||
<i class="fas fa-hashtag"></i> Reference No
|
||||
</label>
|
||||
<input type="text" name="reference_no" class="form-control-compact"
|
||||
<input type="text"
|
||||
name="reference_no"
|
||||
class="form-control-compact"
|
||||
placeholder="Enter reference number">
|
||||
</div>
|
||||
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-money-bill-wave"></i>
|
||||
Amount (₹)
|
||||
<i class="fas fa-money-bill-wave"></i> Amount
|
||||
</label>
|
||||
<input type="number" name="amount" id="installmentAmount"
|
||||
class="form-control-compact" step="0.01" min="1"
|
||||
max="{{ $remaining }}" required
|
||||
<input type="number"
|
||||
name="amount"
|
||||
id="installmentAmount"
|
||||
class="form-control-compact"
|
||||
step="0.01"
|
||||
min="1"
|
||||
max="{{ $remaining }}"
|
||||
required
|
||||
placeholder="Enter installment amount">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-end mt-2">
|
||||
<button type="submit" class="btn-success-compact btn-compact" id="installmentSubmitBtn">
|
||||
<button type="submit"
|
||||
class="btn-success-compact btn-compact"
|
||||
id="installmentSubmitBtn">
|
||||
<i class="fas fa-paper-plane me-2"></i>Submit Installment
|
||||
</button>
|
||||
</div>
|
||||
@@ -617,10 +639,16 @@ body {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Installment History -->
|
||||
{{-- Installment History --}}
|
||||
<h6 class="fw-bold mb-2 text-dark">
|
||||
<i class="fas fa-history me-2"></i>Installment History
|
||||
</h6>
|
||||
|
||||
<div id="noInstallmentsMsg"
|
||||
class="{{ $invoice->installments->count() ? 'd-none' : '' }} text-center text-muted fw-bold py-4">
|
||||
No installments found. Click Add Installment to create one.
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table-compact">
|
||||
<thead>
|
||||
@@ -640,7 +668,7 @@ body {
|
||||
<td>{{ $i->installment_date }}</td>
|
||||
<td>
|
||||
<span class="badge-compact bg-primary bg-opacity-10 text-primary">
|
||||
{{ ucfirst($i->payment_method) }}
|
||||
{{ strtoupper($i->payment_method) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@@ -650,7 +678,9 @@ body {
|
||||
<span class="text-muted">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="fw-bold text-success">₹{{ number_format($i->amount, 2) }}</td>
|
||||
<td class="fw-bold text-success">
|
||||
₹{{ number_format($i->amount, 2) }}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn-danger-compact btn-compact btn-sm deleteInstallment">
|
||||
<i class="fas fa-trash me-1"></i>Delete
|
||||
@@ -665,40 +695,29 @@ body {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add this just above the table -->
|
||||
<div id="noInstallmentsMsg" class="d-none text-center text-muted fw-bold py-4">
|
||||
No installments found. Click "Add Installment" to create one.
|
||||
</div>
|
||||
<table ...>
|
||||
<tbody id="installmentTable">
|
||||
@foreach($invoice->installments as $i)
|
||||
...
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Toggle Installment Form
|
||||
const toggleBtn = document.getElementById("toggleInstallmentForm");
|
||||
const formBox = document.getElementById("installmentForm");
|
||||
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener("click", () => {
|
||||
if (toggleBtn && formBox) {
|
||||
toggleBtn.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
formBox.classList.toggle("d-none");
|
||||
});
|
||||
}
|
||||
|
||||
// Add Installment
|
||||
const submitForm = document.getElementById("installmentSubmitForm");
|
||||
const submitBtn = document.getElementById("installmentSubmitBtn");
|
||||
|
||||
const formatINR = amt =>
|
||||
"₹" + Number(amt).toLocaleString("en-IN", {
|
||||
const formatINR = (amt) => {
|
||||
return Number(amt).toLocaleString("en-IN", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
};
|
||||
|
||||
if (submitForm && submitBtn) {
|
||||
submitForm.addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -708,8 +727,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
fetch("{{ route('admin.invoice.installment.store', $invoice->id) }}", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": submitForm.querySelector("input[name=_token]").value,
|
||||
"Accept": "application/json"
|
||||
"X-CSRF-TOKEN": submitForm.querySelector('input[name="_token"]').value,
|
||||
"Accept": "application/json",
|
||||
},
|
||||
body: new FormData(submitForm)
|
||||
})
|
||||
@@ -723,9 +742,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
return;
|
||||
}
|
||||
|
||||
const table = document.querySelector("#installmentTable");
|
||||
const table = document.getElementById("installmentTable");
|
||||
const index = table.rows.length + 1;
|
||||
|
||||
document.getElementById("noInstallmentsMsg")?.classList.add("d-none");
|
||||
|
||||
table.insertAdjacentHTML("beforeend", `
|
||||
<tr data-id="${data.installment.id}">
|
||||
<td class="fw-bold text-muted">${index}</td>
|
||||
@@ -735,8 +756,12 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
${data.installment.payment_method.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td>${data.installment.reference_no || '-'}</td>
|
||||
<td class="fw-bold text-success">${formatINR(data.installment.amount)}</td>
|
||||
<td>
|
||||
${data.installment.reference_no ? `<span class="text-muted">${data.installment.reference_no}</span>` : '<span class="text-muted">-</span>'}
|
||||
</td>
|
||||
<td class="fw-bold text-success">
|
||||
₹${formatINR(data.installment.amount)}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn-danger-compact btn-compact btn-sm deleteInstallment">
|
||||
<i class="fas fa-trash me-1"></i>Delete
|
||||
@@ -745,25 +770,32 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
</tr>
|
||||
`);
|
||||
|
||||
// Update all displayed values using GST fields!
|
||||
if (document.getElementById("paidAmount")) document.getElementById("paidAmount").textContent = formatINR(data.totalPaid);
|
||||
if (document.getElementById("remainingAmount")) document.getElementById("remainingAmount").textContent = formatINR(data.remaining);
|
||||
if (document.getElementById("baseAmount")) document.getElementById("baseAmount").textContent = formatINR(data.baseAmount);
|
||||
if (document.getElementById("gstAmount")) document.getElementById("gstAmount").textContent = formatINR(data.gstAmount);
|
||||
if (document.getElementById("totalInvoiceWithGst")) document.getElementById("totalInvoiceWithGst").textContent = formatINR(data.finalAmountWithGst);
|
||||
if (document.getElementById("invoiceStatus")) document.getElementById("invoiceStatus").textContent = data.isCompleted ? "Paid" : "Pending";
|
||||
if (document.getElementById("paidAmount")) {
|
||||
document.getElementById("paidAmount").textContent = "₹" + formatINR(data.totalPaid);
|
||||
}
|
||||
if (document.getElementById("remainingAmount")) {
|
||||
document.getElementById("remainingAmount").textContent = "₹" + formatINR(data.remaining);
|
||||
}
|
||||
if (document.getElementById("baseAmount")) {
|
||||
document.getElementById("baseAmount").textContent = "₹" + formatINR(data.baseAmount);
|
||||
}
|
||||
if (document.getElementById("gstAmount")) {
|
||||
document.getElementById("gstAmount").textContent = "₹" + formatINR(data.gstAmount);
|
||||
}
|
||||
if (document.getElementById("totalInvoiceWithGst")) {
|
||||
document.getElementById("totalInvoiceWithGst").textContent = "₹" + formatINR(data.finalAmountWithGst);
|
||||
}
|
||||
|
||||
// Update summary cards if used
|
||||
const paidCard = document.querySelector(".summary-card-compact.paid .summary-value-compact");
|
||||
if (paidCard) paidCard.textContent = formatINR(data.totalPaid);
|
||||
if (paidCard) paidCard.textContent = "₹" + formatINR(data.totalPaid);
|
||||
|
||||
const remainingCard = document.querySelector(".summary-card-compact.remaining .summary-value-compact");
|
||||
if (remainingCard) remainingCard.textContent = formatINR(data.remaining);
|
||||
if (remainingCard) remainingCard.textContent = "₹" + formatINR(data.remaining);
|
||||
|
||||
submitForm.reset();
|
||||
|
||||
// If fully paid, disable/add display logic
|
||||
if (data.isCompleted) {
|
||||
toggleBtn?.remove();
|
||||
if (data.isCompleted && toggleBtn) {
|
||||
toggleBtn.remove();
|
||||
formBox.classList.add("d-none");
|
||||
}
|
||||
|
||||
@@ -775,44 +807,61 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
alert("Something went wrong. Please try again.");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Delete Installment
|
||||
document.addEventListener("click", function (e) {
|
||||
if (!e.target.classList.contains("deleteInstallment")) return;
|
||||
if (!e.target.classList.contains("deleteInstallment") &&
|
||||
!e.target.closest(".deleteInstallment")) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
if (!confirm("Are you sure you want to delete this installment?")) return;
|
||||
if (!confirm("Are you sure you want to delete this installment?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const row = e.target.closest("tr");
|
||||
const btn = e.target.closest(".deleteInstallment");
|
||||
const row = btn.closest("tr");
|
||||
const id = row.getAttribute("data-id");
|
||||
|
||||
fetch("{{ url('/admin/installment') }}/" + id, {
|
||||
fetch("{{ url('admin/installment') }}/" + id, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": "{{ csrf_token() }}",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').getAttribute("content"),
|
||||
"Accept": "application/json",
|
||||
},
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === "success") {
|
||||
row.style.opacity = "0";
|
||||
row.style.opacity = 0;
|
||||
setTimeout(() => row.remove(), 300);
|
||||
|
||||
// Update all displayed values using GST fields!
|
||||
if (document.getElementById("paidAmount")) document.getElementById("paidAmount").textContent = formatINR(data.totalPaid);
|
||||
if (document.getElementById("remainingAmount")) document.getElementById("remainingAmount").textContent = formatINR(data.remaining);
|
||||
if (document.getElementById("baseAmount")) document.getElementById("baseAmount").textContent = formatINR(data.baseAmount);
|
||||
if (document.getElementById("gstAmount")) document.getElementById("gstAmount").textContent = formatINR(data.gstAmount);
|
||||
if (document.getElementById("totalInvoiceWithGst")) document.getElementById("totalInvoiceWithGst").textContent = formatINR(data.finalAmountWithGst);
|
||||
if (document.getElementById("invoiceStatus")) document.getElementById("invoiceStatus").textContent = data.remaining === 0 ? "Paid" : "Pending";
|
||||
if (document.getElementById("paidAmount")) {
|
||||
document.getElementById("paidAmount").textContent = "₹" + formatINR(data.totalPaid);
|
||||
}
|
||||
if (document.getElementById("remainingAmount")) {
|
||||
document.getElementById("remainingAmount").textContent = "₹" + formatINR(data.remaining);
|
||||
}
|
||||
if (document.getElementById("baseAmount")) {
|
||||
document.getElementById("baseAmount").textContent = "₹" + formatINR(data.baseAmount);
|
||||
}
|
||||
if (document.getElementById("gstAmount")) {
|
||||
document.getElementById("gstAmount").textContent = "₹" + formatINR(data.gstAmount);
|
||||
}
|
||||
if (document.getElementById("totalInvoiceWithGst")) {
|
||||
document.getElementById("totalInvoiceWithGst").textContent = "₹" + formatINR(data.finalAmountWithGst);
|
||||
}
|
||||
|
||||
// Update summary cards
|
||||
const paidCard = document.querySelector(".summary-card-compact.paid .summary-value-compact");
|
||||
if (paidCard) paidCard.textContent = formatINR(data.totalPaid);
|
||||
if (paidCard) paidCard.textContent = "₹" + formatINR(data.totalPaid);
|
||||
|
||||
const remainingCard = document.querySelector(".summary-card-compact.remaining .summary-value-compact");
|
||||
if (remainingCard) remainingCard.textContent = formatINR(data.remaining);
|
||||
|
||||
if (remainingCard) remainingCard.textContent = "₹" + formatINR(data.remaining);
|
||||
|
||||
if (data.isZero) {
|
||||
document.getElementById("noInstallmentsMsg")?.classList.remove("d-none");
|
||||
}
|
||||
|
||||
alert(data.message);
|
||||
} else {
|
||||
@@ -825,6 +874,4 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@endsection
|
||||
@@ -1,13 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>Admin Panel</title>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<!-- ✅ CRITICAL: CSRF Token for Echo -->
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>@yield('page-title', 'Admin Panel')</title>
|
||||
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
|
||||
<style>
|
||||
body {
|
||||
@@ -20,7 +28,6 @@
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* ✨ Sidebar Glass + Animated Highlight Effect */
|
||||
.sidebar {
|
||||
width: 200px;
|
||||
height: 100vh;
|
||||
@@ -39,7 +46,6 @@
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Sidebar collapsed state */
|
||||
.sidebar.collapsed {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
@@ -62,10 +68,11 @@
|
||||
}
|
||||
|
||||
.sidebar .word {
|
||||
color: #800000; font-size: 13px; line-height: 1.24;
|
||||
color: #800000;
|
||||
font-size: 13px;
|
||||
line-height: 1.24;
|
||||
}
|
||||
|
||||
/* 🔥 Sidebar Links */
|
||||
.sidebar a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -82,7 +89,6 @@
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Background Animation */
|
||||
.sidebar a::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -107,7 +113,6 @@
|
||||
color: #1258e0 !important;
|
||||
}
|
||||
|
||||
/* Icon bounce effect */
|
||||
.sidebar a i {
|
||||
margin-right: 8px;
|
||||
font-size: 1.1rem;
|
||||
@@ -119,7 +124,6 @@
|
||||
color: #1258e0 !important;
|
||||
}
|
||||
|
||||
/* Active link glow effect */
|
||||
.sidebar a.active {
|
||||
background: linear-gradient(90deg, rgba(80,120,255,0.15), rgba(120,180,255,0.2));
|
||||
color: #1258e0 !important;
|
||||
@@ -134,7 +138,6 @@
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
/* Logout Button */
|
||||
.sidebar form button {
|
||||
border-radius: 12px;
|
||||
margin-top: 12px;
|
||||
@@ -148,7 +151,6 @@
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
/* 🧭 Main Content */
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
min-height: 100vh;
|
||||
@@ -160,13 +162,11 @@
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Main content when sidebar is collapsed */
|
||||
.main-content.expanded {
|
||||
margin-left: 0;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
/* Header hamburger button */
|
||||
.header-toggle {
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -220,12 +220,16 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="sidebar">
|
||||
<div class="logo">
|
||||
<img src="{{ asset('images/kent_logo2.png') }}" alt="Kent Logo">
|
||||
<div class="word"><strong>KENT</strong><br /><small>International Pvt. Ltd.</small></div>
|
||||
<div class="word">
|
||||
<strong>KENT</strong><br />
|
||||
<small>International Pvt. Ltd.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Dashboard (requires order.view) --}}
|
||||
@@ -234,12 +238,19 @@
|
||||
<i class="bi bi-house"></i> Dashboard
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
<!--
|
||||
{{-- Shipments --}}
|
||||
@can('shipment.view')
|
||||
<a href="{{ route('admin.shipments') }}" class="{{ request()->routeIs('admin.shipments') ? 'active' : '' }}">
|
||||
<i class="bi bi-truck"></i> Shipments
|
||||
</a>
|
||||
@endcan -->
|
||||
|
||||
{{-- Container – NEW MENU --}}
|
||||
@can('container.view')
|
||||
<a href="{{ route('containers.index') }}" class="{{ request()->routeIs('containers.*') ? 'active' : '' }}">
|
||||
<i class="fa-solid fa-box"></i> Container
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Invoice --}}
|
||||
@@ -263,10 +274,12 @@
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Chat Support (NO PERMISSION REQUIRED) --}}
|
||||
{{-- Chat Support --}}
|
||||
@can('chat_support.view')
|
||||
<a href="{{ route('admin.chat_support') }}" class="{{ request()->routeIs('admin.chat_support') ? 'active' : '' }}">
|
||||
<i class="bi bi-chat-dots"></i> Chat Support
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Orders --}}
|
||||
@can('orders.view')
|
||||
@@ -284,15 +297,17 @@
|
||||
|
||||
{{-- Profile Update Requests --}}
|
||||
@can('request.update_profile')
|
||||
<a href="{{ route('admin.profile.requests') }}">
|
||||
<a href="{{ route('admin.profile.requests') }}" class="{{ request()->routeIs('admin.profile.requests') ? 'active' : '' }}">
|
||||
<i class="bi bi-person-lines-fill"></i> Profile Update Requests
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Staff (NO PERMISSION REQUIRED) --}}
|
||||
{{-- Staff --}}
|
||||
@can('staff.view')
|
||||
<a href="{{ route('admin.staff.index') }}" class="{{ request()->routeIs('admin.staff.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-person-badge"></i> Staff
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Account Section --}}
|
||||
@can('account.view')
|
||||
@@ -325,40 +340,49 @@
|
||||
<div class="dropdown">
|
||||
<a href="#" class="d-flex align-items-center text-decoration-none dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<img src="https://i.pravatar.cc/40" class="rounded-circle me-2" width="35" height="35">
|
||||
<span class="dropdown-user-profile-name">{{ Auth::guard('admin')->user()->name ?? 'User' }}</span>
|
||||
<span class="dropdown-user-profile-name">
|
||||
{{ auth('admin')->user()->name ?? 'User' }}
|
||||
</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="{{ route('admin.profile') }}"><i class="bi bi-person-circle me-2"></i>Profile</a></li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ route('admin.profile') }}">
|
||||
<i class="bi bi-person-circle me-2"></i>Profile
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form method="POST" action="{{ route('admin.logout') }}">
|
||||
@csrf
|
||||
<button class="dropdown-item" type="submit"><i class="bi bi-box-arrow-right me-2"></i>Logout</button>
|
||||
<button class="dropdown-item" type="submit">
|
||||
<i class="bi bi-box-arrow-right me-2"></i>Logout
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
@yield('content')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
@vite(['resources/js/app.js'])
|
||||
@yield('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const headerToggle = document.getElementById('headerToggle');
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
|
||||
// Function to toggle sidebar
|
||||
function toggleSidebar() {
|
||||
sidebar.classList.toggle('collapsed');
|
||||
mainContent.classList.toggle('expanded');
|
||||
}
|
||||
|
||||
// Header toggle button click event
|
||||
if (headerToggle) {
|
||||
headerToggle.addEventListener('click', toggleSidebar);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Professional Invoice</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<link rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--primary: #2c3e50;
|
||||
@@ -21,7 +23,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f5f7fa;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
@@ -30,14 +32,14 @@
|
||||
.invoice-container {
|
||||
max-width: 1200px;
|
||||
margin: 2rem auto;
|
||||
background: white;
|
||||
background: #ffffff;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.invoice-header {
|
||||
background: white;
|
||||
background: #ffffff;
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
@@ -56,11 +58,11 @@
|
||||
}
|
||||
|
||||
.id-container {
|
||||
margin-bottom: 1rem; /* Reduced from 1.5rem */
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.id-box {
|
||||
background: white;
|
||||
background: #ffffff;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1rem;
|
||||
border: 1px solid #e9ecef;
|
||||
@@ -82,10 +84,6 @@
|
||||
border-left: 4px solid var(--success);
|
||||
}
|
||||
|
||||
.id-box-accent {
|
||||
border-left: 4px solid var(--warning);
|
||||
}
|
||||
|
||||
.id-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
@@ -93,7 +91,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.5rem; /* Reduced from 0.75rem */
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@@ -107,11 +105,6 @@
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.id-icon-accent {
|
||||
background: rgba(243, 156, 18, 0.1);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.id-label {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
@@ -129,10 +122,10 @@
|
||||
}
|
||||
|
||||
.date-container {
|
||||
background: white;
|
||||
background: #ffffff;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1rem; /* Reduced from 1.25rem */
|
||||
margin-bottom: 1rem; /* Reduced from 1.5rem */
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #e9ecef;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
@@ -152,9 +145,9 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 0.5rem; /* Reduced from 0.75rem */
|
||||
margin: 0 auto 0.5rem;
|
||||
background: var(--secondary);
|
||||
color: white;
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@@ -172,7 +165,7 @@
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
padding: 0.5rem;
|
||||
background: white;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
@@ -195,7 +188,7 @@
|
||||
.card {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 1rem; /* Reduced from 1.5rem */
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
@@ -235,18 +228,7 @@
|
||||
|
||||
.summary-header {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.amount-row {
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
border-top: 2px solid #dee2e6;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
@@ -266,17 +248,16 @@
|
||||
padding: 0.35rem 0.65rem;
|
||||
}
|
||||
|
||||
/* COMPACT HEADER STYLES */
|
||||
.compact-header {
|
||||
margin-bottom: 0.75rem; /* Reduced from default */
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.compact-header .invoice-title {
|
||||
margin-bottom: 0.25rem; /* Reduced gap */
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.compact-header .status-badge {
|
||||
margin-top: 0.25rem; /* Reduced gap */
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -301,13 +282,11 @@
|
||||
<body>
|
||||
<div class="invoice-container">
|
||||
<div class="p-4">
|
||||
<!-- ============================
|
||||
INVOICE HEADER - COMPACT
|
||||
============================ -->
|
||||
@php
|
||||
$showActions = $showActions ?? true;
|
||||
@endphp
|
||||
|
||||
{{-- HEADER --}}
|
||||
<div class="compact-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
@@ -316,7 +295,6 @@
|
||||
</h2>
|
||||
<h4 class="fw-bold text-dark mb-0">{{ $invoice->invoice_number }}</h4>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 text-end">
|
||||
<span class="status-badge
|
||||
@if($invoice->status == 'paid') bg-success
|
||||
@@ -334,11 +312,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Three ID Boxes in One Row -->
|
||||
{{-- ID BOXES: INVOICE + CONTAINER --}}
|
||||
<div class="id-container">
|
||||
<div class="row">
|
||||
<!-- Invoice ID Box -->
|
||||
<div class="col-md-4 mb-3">
|
||||
{{-- Invoice ID Box --}}
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="id-box id-box-primary">
|
||||
<div class="id-icon id-icon-primary">
|
||||
<i class="fas fa-receipt"></i>
|
||||
@@ -348,18 +326,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order ID Box -->
|
||||
<div class="col-md-4 mb-3">
|
||||
{{-- Container ID Box --}}
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="id-box id-box-secondary">
|
||||
<div class="id-icon id-icon-secondary">
|
||||
<i class="fas fa-shopping-cart"></i>
|
||||
<i class="fas fa-box"></i>
|
||||
</div>
|
||||
<div class="id-label">Order ID</div>
|
||||
<div class="id-label">Container ID</div>
|
||||
<div class="id-value">
|
||||
@if($invoice->order && $invoice->order->order_id)
|
||||
{{ $invoice->order->order_id }}
|
||||
@elseif($invoice->order_id)
|
||||
{{ $invoice->order_id }}
|
||||
@if($invoice->container && $invoice->container->container_number)
|
||||
{{ $invoice->container->container_number }}
|
||||
@elseif($invoice->container_id)
|
||||
{{ $invoice->container_id }}
|
||||
@else
|
||||
N/A
|
||||
@endif
|
||||
@@ -367,35 +345,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shipment ID Box -->
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="id-box id-box-accent">
|
||||
<div class="id-icon id-icon-accent">
|
||||
<i class="fas fa-shipping-fast"></i>
|
||||
</div>
|
||||
<div class="id-label">Shipment ID</div>
|
||||
<div class="id-value">
|
||||
@php
|
||||
$shipmentId = 'N/A';
|
||||
// Try multiple ways to get shipment ID
|
||||
if($invoice->shipment && $invoice->shipment->shipment_id) {
|
||||
$shipmentId = $invoice->shipment->shipment_id;
|
||||
} elseif($invoice->shipment_id) {
|
||||
$shipmentId = $invoice->shipment_id;
|
||||
} elseif(isset($shipment) && $shipment && $shipment->shipment_id) {
|
||||
$shipmentId = $shipment->shipment_id;
|
||||
}
|
||||
@endphp
|
||||
{{ $shipmentId }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================
|
||||
DATES SECTION
|
||||
============================ -->
|
||||
{{-- DATES --}}
|
||||
<div class="date-container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-5">
|
||||
@@ -404,7 +355,9 @@
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
</div>
|
||||
<div class="date-label">INVOICE DATE</div>
|
||||
<div class="date-value">{{ \Carbon\Carbon::parse($invoice->invoice_date)->format('M d, Y') }}</div>
|
||||
<div class="date-value">
|
||||
{{ \Carbon\Carbon::parse($invoice->invoice_date)->format('M d, Y') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
@@ -426,9 +379,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================
|
||||
CUSTOMER DETAILS
|
||||
============================ -->
|
||||
{{-- CUSTOMER DETAILS --}}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0 fw-bold">
|
||||
@@ -464,9 +415,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================
|
||||
INVOICE ITEMS
|
||||
============================ -->
|
||||
{{-- INVOICE ITEMS (EDITABLE WHEN EMBEDDED) --}}
|
||||
@php
|
||||
$isEmbedded = isset($embedded) && $embedded;
|
||||
@endphp
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0 fw-bold">
|
||||
@@ -474,6 +427,13 @@
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
|
||||
@if($isEmbedded)
|
||||
<form action="{{ route('admin.invoices.items.update', $invoice->id) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
@endif
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
@@ -502,8 +462,35 @@
|
||||
<td class="text-center">{{ $item->qty }}</td>
|
||||
<td class="text-center fw-bold">{{ $item->ttl_qty }}</td>
|
||||
<td class="text-center">{{ $item->unit }}</td>
|
||||
<td class="text-center text-success fw-bold">₹{{ number_format($item->price,2) }}</td>
|
||||
<td class="text-center text-primary fw-bold">₹{{ number_format($item->ttl_amount,2) }}</td>
|
||||
|
||||
@if($isEmbedded)
|
||||
{{-- EDIT MODE (invoice_edit page) --}}
|
||||
<td class="text-center" style="width:120px;">
|
||||
<input type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
name="items[{{ $item->id }}][price]"
|
||||
value="{{ old('items.' . $item->id . '.price', $item->price) }}"
|
||||
class="form-control form-control-sm text-end">
|
||||
</td>
|
||||
<td class="text-center" style="width:140px;">
|
||||
<input type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
name="items[{{ $item->id }}][ttl_amount]"
|
||||
value="{{ old('items.' . $item->id . '.ttl_amount', $item->ttl_amount) }}"
|
||||
class="form-control form-control-sm text-end">
|
||||
</td>
|
||||
@else
|
||||
{{-- NORMAL POPUP (READ-ONLY) --}}
|
||||
<td class="text-center text-success fw-bold">
|
||||
₹{{ number_format($item->price, 2) }}
|
||||
</td>
|
||||
<td class="text-center text-primary fw-bold">
|
||||
₹{{ number_format($item->ttl_amount, 2) }}
|
||||
</td>
|
||||
@endif
|
||||
|
||||
<td class="text-center">{{ $item->cbm }}</td>
|
||||
<td class="text-center">{{ $item->ttl_cbm }}</td>
|
||||
<td class="text-center">{{ $item->kg }}</td>
|
||||
@@ -513,15 +500,30 @@
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
||||
@if($invoice->items->isEmpty())
|
||||
<tr>
|
||||
<td colspan="13" class="text-center text-muted fw-bold py-3">
|
||||
No invoice items found.
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if($isEmbedded)
|
||||
<div class="text-end p-3">
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-save me-1"></i> Update Items & Recalculate
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================
|
||||
FINAL SUMMARY
|
||||
============================ -->
|
||||
{{-- FINAL SUMMARY --}}
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-6">
|
||||
<div class="card summary-card">
|
||||
@@ -533,61 +535,78 @@
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">Amount:</span>
|
||||
<span class="fw-bold text-dark">₹{{ number_format($invoice->final_amount,2) }}</span>
|
||||
<span class="fw-bold text-dark">
|
||||
₹{{ number_format($invoice->final_amount, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@if($invoice->tax_type === 'gst')
|
||||
{{-- CGST --}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">CGST ({{ $invoice->cgst_percent ?? ($invoice->gst_percent/2) }}%):</span>
|
||||
<span class="fw-bold text-danger">₹{{ number_format($invoice->gst_amount/2, 2) }}</span>
|
||||
<span class="fw-semibold">
|
||||
CGST ({{ $invoice->cgst_percent ?? ($invoice->gst_percent / 2) }}%):
|
||||
</span>
|
||||
<span class="fw-bold text-danger">
|
||||
₹{{ number_format($invoice->gst_amount / 2, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{-- SGST --}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">SGST ({{ $invoice->sgst_percent ?? ($invoice->gst_percent/2) }}%):</span>
|
||||
<span class="fw-bold text-danger">₹{{ number_format($invoice->gst_amount/2, 2) }}</span>
|
||||
<span class="fw-semibold">
|
||||
SGST ({{ $invoice->sgst_percent ?? ($invoice->gst_percent / 2) }}%):
|
||||
</span>
|
||||
<span class="fw-bold text-danger">
|
||||
₹{{ number_format($invoice->gst_amount / 2, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@elseif($invoice->tax_type === 'igst')
|
||||
{{-- IGST --}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">IGST ({{ $invoice->igst_percent ?? $invoice->gst_percent }}%):</span>
|
||||
<span class="fw-bold text-danger">₹{{ number_format($invoice->gst_amount, 2) }}</span>
|
||||
<span class="fw-semibold">
|
||||
IGST ({{ $invoice->igst_percent ?? $invoice->gst_percent }}%):
|
||||
</span>
|
||||
<span class="fw-bold text-danger">
|
||||
₹{{ number_format($invoice->gst_amount, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
@else
|
||||
{{-- Default GST --}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">GST ({{ $invoice->gst_percent }}%):</span>
|
||||
<span class="fw-bold text-danger">₹{{ number_format($invoice->gst_amount, 2) }}</span>
|
||||
<span class="fw-semibold">
|
||||
GST ({{ $invoice->gst_percent }}%):
|
||||
</span>
|
||||
<span class="fw-bold text-danger">
|
||||
₹{{ number_format($invoice->gst_amount, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center pt-1">
|
||||
<span class="fw-bold text-dark">Total Payable:</span>
|
||||
<span class="fw-bold text-success">₹{{ number_format($invoice->final_amount_with_gst,2) }}</span>
|
||||
<span class="fw-bold text-success">
|
||||
₹{{ number_format($invoice->final_amount_with_gst, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================
|
||||
FOOTER — DOWNLOAD & SHARE
|
||||
============================ -->
|
||||
{{-- FOOTER ACTIONS --}}
|
||||
<div class="mt-4 pt-3 border-top text-center">
|
||||
@if($invoice->pdf_path)
|
||||
<a href="{{ asset($invoice->pdf_path) }}" class="btn btn-primary me-2" download>
|
||||
@if($invoice->pdf_path && $showActions)
|
||||
<a href="{{ asset($invoice->pdf_path) }}"
|
||||
class="btn btn-primary me-2" download>
|
||||
<i class="fas fa-download me-1"></i> Download PDF
|
||||
</a>
|
||||
|
||||
<button class="btn btn-success" onclick="shareInvoice()">
|
||||
<i class="fas fa-share me-1"></i> Share
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Footer Message -->
|
||||
{{-- FOOTER MESSAGE --}}
|
||||
<div class="mt-4 pt-3 border-top text-center text-muted">
|
||||
<p class="mb-1">Thank you for your business!</p>
|
||||
<p class="mb-0">For any inquiries, contact us at support@Kent Logistic</p>
|
||||
@@ -596,10 +615,6 @@
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- ============================
|
||||
SHARE SCRIPT
|
||||
============================ -->
|
||||
<script>
|
||||
function shareInvoice() {
|
||||
const shareData = {
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\RequestController;
|
||||
use App\Http\Controllers\UserAuthController;
|
||||
use App\Http\Controllers\MarkListController;
|
||||
use App\Http\Controllers\User\UserOrderController;
|
||||
use App\Http\Controllers\User\UserProfileController;
|
||||
use App\Http\Controllers\User\ChatController;
|
||||
|
||||
|
||||
|
||||
Broadcast::routes(['middleware' => ['auth:api']]);
|
||||
|
||||
|
||||
|
||||
//user send request
|
||||
@@ -46,4 +57,22 @@ Route::middleware(['auth:api'])->group(function () {
|
||||
Route::post('/user/profile-update-request', [UserProfileController::class, 'updateProfileRequest']);
|
||||
|
||||
// Route::post('/user/profile/update', [UserProfileController::class, 'updateProfile']);
|
||||
|
||||
// ===========================
|
||||
// CHAT SUPPORT ROUTES
|
||||
// ===========================
|
||||
Route::get('/user/chat/start', [ChatController::class, 'startChat']);
|
||||
Route::get('/user/chat/messages/{ticketId}', [ChatController::class, 'getMessages']);
|
||||
Route::post('/user/chat/send/{ticketId}', [ChatController::class, 'sendMessage']);
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
Route::post('/broadcasting/auth', function (Request $request) {
|
||||
if (!auth()->check()) {
|
||||
return response()->json(['message' => 'Unauthenticated'], 401);
|
||||
}
|
||||
|
||||
return Broadcast::auth($request);
|
||||
})->middleware('auth:api');
|
||||
|
||||
21
routes/broadcasting.php
Normal file
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']);
|
||||
42
routes/channels.php
Normal file
42
routes/channels.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
use App\Models\SupportTicket;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Broadcast Channels
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
Broadcast::channel('ticket.{ticketId}', function ($user, $ticketId) {
|
||||
|
||||
\Log::info('🔐 Broadcasting Auth Check', [
|
||||
'ticketId' => $ticketId,
|
||||
'user_id' => $user->id ?? 'NULL',
|
||||
'user_table' => method_exists($user, 'getTable') ? $user->getTable() : 'unknown',
|
||||
'user_class' => get_class($user)
|
||||
]);
|
||||
|
||||
$ticket = SupportTicket::find($ticketId);
|
||||
|
||||
if (!$ticket) {
|
||||
\Log::warning('❌ Ticket not found', ['ticketId' => $ticketId]);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ✅ Admin/Staff Check (Session Auth)
|
||||
if (get_class($user) === 'App\Models\Admin') {
|
||||
\Log::info('✅ Admin authorized for ticket', ['admin_id' => $user->id]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ✅ User Check (JWT Auth - must own ticket)
|
||||
if (get_class($user) === 'App\Models\User' && $ticket->user_id === $user->id) {
|
||||
\Log::info('✅ User authorized for own ticket', ['user_id' => $user->id]);
|
||||
return true;
|
||||
}
|
||||
|
||||
\Log::warning('❌ Authorization failed');
|
||||
return false;
|
||||
});
|
||||
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
|
||||
109
routes/web.php
109
routes/web.php
@@ -11,6 +11,10 @@ use App\Http\Controllers\Admin\AdminCustomerController;
|
||||
use App\Http\Controllers\Admin\AdminAccountController;
|
||||
use App\Http\Controllers\Admin\AdminReportController;
|
||||
use App\Http\Controllers\Admin\AdminStaffController;
|
||||
use App\Http\Controllers\Admin\AdminChatController;
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
use App\Http\Controllers\ContainerController;
|
||||
|
||||
|
||||
// ---------------------------
|
||||
// Public Front Page
|
||||
@@ -19,6 +23,41 @@ Route::get('/', function () {
|
||||
return view('welcome');
|
||||
});
|
||||
|
||||
|
||||
|
||||
// ADD THIS BEFORE YOUR ADMIN ROUTES (Around Line 20-25)
|
||||
|
||||
// ==========================================
|
||||
// BROADCASTING AUTH (FOR ADMIN SESSION)
|
||||
// ==========================================
|
||||
Broadcast::routes(['middleware' => ['web']]);
|
||||
|
||||
// Custom broadcasting auth for admin guard
|
||||
Route::post('/broadcasting/auth', function (\Illuminate\Http\Request $request) {
|
||||
|
||||
\Log::info('🎯 Broadcasting Auth Request', [
|
||||
'channel' => $request->input('channel_name'),
|
||||
'admin_check' => auth('admin')->check(),
|
||||
'web_check' => auth('web')->check(),
|
||||
]);
|
||||
|
||||
// ✅ Admin Guard (Session)
|
||||
if (auth('admin')->check()) {
|
||||
\Log::info('✅ Admin authenticated', ['id' => auth('admin')->id()]);
|
||||
return Broadcast::auth($request);
|
||||
}
|
||||
|
||||
// ✅ Web Guard (Fallback for Users)
|
||||
if (auth('web')->check()) {
|
||||
\Log::info('✅ Web user authenticated', ['id' => auth('web')->id()]);
|
||||
return Broadcast::auth($request);
|
||||
}
|
||||
|
||||
\Log::warning('❌ No authentication found');
|
||||
return response()->json(['message' => 'Unauthenticated'], 403);
|
||||
|
||||
})->middleware('web');
|
||||
|
||||
// ---------------------------
|
||||
// ADMIN LOGIN ROUTES
|
||||
// ---------------------------
|
||||
@@ -27,9 +66,15 @@ Route::prefix('admin')->group(function () {
|
||||
Route::get('/login', [AdminAuthController::class, 'showLoginForm'])->name('admin.login');
|
||||
Route::post('/login', [AdminAuthController::class, 'login'])->name('admin.login.submit');
|
||||
Route::post('/logout', [AdminAuthController::class, 'logout'])->name('admin.logout');
|
||||
|
||||
|
||||
});
|
||||
|
||||
//Broadcast::routes([
|
||||
//'middleware' => ['web', 'auth:admin'],
|
||||
//]);
|
||||
|
||||
Broadcast::routes(['middleware' => ['web']]);
|
||||
// ==========================================
|
||||
// PROTECTED ADMIN ROUTES (session protected)
|
||||
// ==========================================
|
||||
@@ -56,6 +101,33 @@ Route::prefix('admin')
|
||||
->name('admin.profile');
|
||||
|
||||
|
||||
|
||||
//---------------------------
|
||||
// CONTAINER ROUTES
|
||||
//---------------------------
|
||||
|
||||
Route::get('/containers', [ContainerController::class, 'index'])
|
||||
->name('containers.index');
|
||||
|
||||
// Container create form
|
||||
Route::get('/containers/create', [ContainerController::class, 'create'])
|
||||
->name('containers.create');
|
||||
|
||||
// Container store
|
||||
Route::post('/containers', [ContainerController::class, 'store'])
|
||||
->name('containers.store');
|
||||
|
||||
// Container delete
|
||||
Route::resource('containers', ContainerController::class);
|
||||
|
||||
//status update
|
||||
Route::post('containers/{container}/status', [ContainerController::class, 'updateStatus'])
|
||||
->name('containers.update-status');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ---------------------------
|
||||
// USER REQUESTS
|
||||
// ---------------------------
|
||||
@@ -199,6 +271,11 @@ Route::prefix('admin')
|
||||
->name('admin.invoice.installment.delete');
|
||||
|
||||
|
||||
Route::put('admin/invoices/{invoice}/items', [AdminInvoiceController::class, 'updateItems'])
|
||||
->name('admin.invoices.items.update');
|
||||
|
||||
|
||||
|
||||
//Add New Invoice
|
||||
Route::get('/admin/invoices/create', [InvoiceController::class, 'create'])->name('admin.invoices.create');
|
||||
|
||||
@@ -220,6 +297,19 @@ Route::prefix('admin')
|
||||
|
||||
Route::post('/customers/{id}/status', [AdminCustomerController::class, 'toggleStatus'])
|
||||
->name('admin.customers.status');
|
||||
|
||||
|
||||
// Chat list page
|
||||
Route::get('/chat-support', [AdminChatController::class, 'index'])
|
||||
->name('admin.chat_support');
|
||||
|
||||
// Chat window (open specific user's chat)
|
||||
Route::get('/chat-support/{ticketId}', [AdminChatController::class, 'openChat'])
|
||||
->name('admin.chat.open');
|
||||
|
||||
// Admin sending message
|
||||
Route::post('/chat-support/{ticketId}/send', [AdminChatController::class, 'sendMessage'])
|
||||
->name('admin.chat.send');
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
@@ -302,3 +392,22 @@ Route::prefix('admin')
|
||||
// staff resource
|
||||
Route::resource('staff', AdminStaffController::class);
|
||||
});
|
||||
|
||||
// routes/web.php Line 57 (admin routes PREFIX ANTA)
|
||||
Route::prefix('admin')->middleware('auth:admin')->group(function () {
|
||||
// ... your routes
|
||||
});
|
||||
|
||||
Route::post('/admin/broadcasting/auth', function () {
|
||||
return Broadcast::auth(request());
|
||||
})->middleware('auth:admin');
|
||||
|
||||
|
||||
// Container row update route
|
||||
Route::post('/containers/{container}/update-rows', [ContainerController::class, 'updateRows'])
|
||||
->name('containers.rows.update');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@ import tailwindcss from '@tailwindcss/vite';
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
laravel({
|
||||
input: ['resources/css/app.css', 'resources/js/app.js'],
|
||||
input: [
|
||||
"resources/css/app.css",
|
||||
"resources/js/app.js",
|
||||
// ❌ Remove echo.js from here - it's imported in app.js
|
||||
],
|
||||
refresh: true,
|
||||
}),
|
||||
tailwindcss(),
|
||||
|
||||
Reference in New Issue
Block a user