Compare commits
14 Commits
dev
...
33571a5fd7
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
33571a5fd7 | ||
|
|
94e211f87e | ||
|
|
8b6d3d5fad | ||
|
|
c89e5bdf7d | ||
|
|
10af713fa1 | ||
|
|
ebb263cd36 | ||
|
|
82d9c10130 | ||
|
|
cb24cf575b | ||
|
|
e4c07cb838 | ||
|
|
f38a5afdd7 | ||
|
|
9423c79c80 | ||
|
|
8a958b9c48 | ||
|
|
82882e859e | ||
|
|
2d28e7c1d5 |
12
.env.example
12
.env.example
@@ -20,12 +20,12 @@ LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=kent_logistics6
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
@@ -4,10 +4,10 @@ namespace App\Events;
|
||||
|
||||
use App\Models\ChatMessage;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class NewChatMessage implements ShouldBroadcastNow
|
||||
class NewChatMessage implements ShouldBroadcast
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
@@ -17,23 +17,30 @@ class NewChatMessage implements ShouldBroadcastNow
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(ChatMessage $message)
|
||||
{
|
||||
// Also load sender polymorphic relationship
|
||||
$message->load('sender');
|
||||
{
|
||||
// Safe data only (no heavy relationships in queue)
|
||||
$this->message = [
|
||||
'id' => $message->id,
|
||||
'ticket_id' => $message->ticket_id,
|
||||
'sender_id' => $message->sender_id,
|
||||
'sender_type' => $message->sender_type,
|
||||
'message' => $message->message,
|
||||
'file_path' => $message->file_path,
|
||||
'file_type' => $message->file_type,
|
||||
'created_at' => $message->created_at->toDateTimeString(),
|
||||
];
|
||||
|
||||
// Load sender separately for broadcastWith()
|
||||
$this->sender = $message->sender;
|
||||
}
|
||||
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* The channel the event should broadcast on.
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return [
|
||||
new PrivateChannel('ticket.' . $this->message->ticket_id),
|
||||
new PrivateChannel('admin.chat') // 👈 ADD THIS
|
||||
];
|
||||
|
||||
return new PrivateChannel('ticket.' . $this->message->ticket_id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,40 +48,25 @@ class NewChatMessage implements ShouldBroadcastNow
|
||||
*/
|
||||
public function broadcastWith()
|
||||
{
|
||||
\Log::info('APP_URL USED IN EVENT', [
|
||||
'url' => config('app.url'),
|
||||
]);
|
||||
|
||||
\Log::info("DEBUG: NewChatMessage broadcasting on channel ticket.".$this->message->ticket_id);
|
||||
|
||||
\Log::info("EVENT BROADCAST FIRED", [
|
||||
'channel' => 'ticket.'.$this->message->ticket_id,
|
||||
'sender_type' => $this->message->sender_type,
|
||||
'sender_id' => $this->message->sender_id,
|
||||
]);
|
||||
|
||||
|
||||
return [
|
||||
'id' => $this->message->id,
|
||||
'ticket_id' => $this->message->ticket_id,
|
||||
'sender_id' => $this->message->sender_id,
|
||||
'sender_type' => $this->message->sender_type,
|
||||
'message' => $this->message->message,
|
||||
'client_id' => $this->message->client_id,
|
||||
|
||||
// ✅ relative path only
|
||||
'file_path' => $this->message->file_path ?? null,
|
||||
'file_type' => $this->message->file_type ?? null,
|
||||
|
||||
'file_url' => $this->message->file_path
|
||||
? asset('storage/' . $this->message->file_path)
|
||||
: null,
|
||||
'file_type' => $this->message->file_type,
|
||||
'sender' => [
|
||||
'id' => $this->message->sender->id,
|
||||
'name' => $this->getSenderName(),
|
||||
'is_admin' => $this->message->sender_type === \App\Models\Admin::class,
|
||||
],
|
||||
|
||||
'created_at' => $this->message->created_at->toDateTimeString(),
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,10 +84,4 @@ class NewChatMessage implements ShouldBroadcastNow
|
||||
// Admin model has ->name
|
||||
return $sender->name ?? "Admin";
|
||||
}
|
||||
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'NewChatMessage';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,14 +3,35 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
<<<<<<< HEAD
|
||||
use App\Models\SupportTicket;
|
||||
use App\Models\ChatMessage;
|
||||
use App\Events\NewChatMessage;
|
||||
use Illuminate\Http\Request;
|
||||
=======
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\SupportTicket;
|
||||
use App\Models\ChatMessage;
|
||||
use App\Events\NewChatMessage;
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
|
||||
class AdminChatController extends Controller
|
||||
{
|
||||
/**
|
||||
<<<<<<< HEAD
|
||||
* Page 1: List all customer chat tickets
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$tickets = SupportTicket::with(['user', 'messages' => function($query) {
|
||||
$query->latest()->limit(1);
|
||||
}])
|
||||
->orderBy('updated_at', 'desc')
|
||||
->get();
|
||||
|
||||
return view('admin.chat_support', compact('tickets'));
|
||||
}
|
||||
=======
|
||||
* Page 1: List all active user chats
|
||||
*/
|
||||
public function index()
|
||||
@@ -28,11 +49,26 @@ class AdminChatController extends Controller
|
||||
return view('admin.chat_support', compact('tickets'));
|
||||
}
|
||||
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
|
||||
/**
|
||||
* Page 2: Open chat window for a specific user
|
||||
*/
|
||||
public function openChat($ticketId)
|
||||
<<<<<<< HEAD
|
||||
{
|
||||
$ticket = SupportTicket::with('user')->findOrFail($ticketId);
|
||||
$messages = ChatMessage::where('ticket_id', $ticketId)
|
||||
->orderBy('created_at', 'asc')
|
||||
->with('sender')
|
||||
->get();
|
||||
|
||||
return view('admin.chat_window', compact('ticket', 'messages'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin sends a message to the user (FIXED - LIVE CHAT)
|
||||
=======
|
||||
{
|
||||
$ticket = SupportTicket::with('user')->findOrFail($ticketId);
|
||||
|
||||
@@ -53,6 +89,7 @@ class AdminChatController extends Controller
|
||||
|
||||
/**
|
||||
* Admin sends a message to the user
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
*/
|
||||
public function sendMessage(Request $request, $ticketId)
|
||||
{
|
||||
@@ -69,9 +106,12 @@ class AdminChatController extends Controller
|
||||
'sender_id' => $admin->id,
|
||||
'sender_type' => \App\Models\Admin::class,
|
||||
'message' => $request->message,
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
'read_by_admin' => true,
|
||||
'read_by_user' => false,
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
];
|
||||
|
||||
// File Upload
|
||||
@@ -85,11 +125,20 @@ class AdminChatController extends Controller
|
||||
$message = ChatMessage::create($data);
|
||||
$message->load('sender');
|
||||
|
||||
<<<<<<< HEAD
|
||||
\Log::info("DEBUG: ChatController sendMessage called", [
|
||||
=======
|
||||
\Log::info("DEBUG: ChatController sendMessage called", [
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
'ticket_id' => $ticketId,
|
||||
'payload' => $request->all()
|
||||
]);
|
||||
|
||||
<<<<<<< HEAD
|
||||
// 🔥 LIVE CHAT - Queue bypass (100% working)
|
||||
broadcast(new NewChatMessage($message))->toOthers();
|
||||
|
||||
=======
|
||||
// Broadcast real-time
|
||||
broadcast(new NewChatMessage($message));
|
||||
|
||||
@@ -97,6 +146,7 @@ class AdminChatController extends Controller
|
||||
'ticket_id' => $ticketId,
|
||||
'payload' => $request->all()
|
||||
]);
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message
|
||||
|
||||
@@ -3,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,11 +29,8 @@ class AdminInvoiceController extends Controller
|
||||
// -------------------------------------------------------------
|
||||
public function popup($id)
|
||||
{
|
||||
$invoice = Invoice::with(['items', 'order', 'installments'])->findOrFail($id);
|
||||
|
||||
$shipment = \App\Models\Shipment::whereHas('items', function ($q) use ($invoice) {
|
||||
$q->where('order_id', $invoice->order_id);
|
||||
})->first();
|
||||
$invoice = Invoice::with(['items', 'customer', 'container'])->findOrFail($id);
|
||||
$shipment = null;
|
||||
|
||||
return view('admin.popup_invoice', compact('invoice', 'shipment'));
|
||||
}
|
||||
@@ -40,24 +40,35 @@ class AdminInvoiceController extends Controller
|
||||
// -------------------------------------------------------------
|
||||
public function edit($id)
|
||||
{
|
||||
$invoice = Invoice::with(['order.shipments'])->findOrFail($id);
|
||||
$shipment = $invoice->order?->shipments?->first();
|
||||
$invoice = Invoice::with(['items', 'customer', 'container'])->findOrFail($id);
|
||||
$shipment = null;
|
||||
|
||||
return view('admin.invoice_edit', compact('invoice', 'shipment'));
|
||||
// ADD THIS SECTION: Calculate customer's total due across all invoices
|
||||
$customerTotalDue = Invoice::where('customer_id', $invoice->customer_id)
|
||||
->where('status', '!=', 'cancelled')
|
||||
->where('status', '!=', 'void')
|
||||
->sum('final_amount_with_gst');
|
||||
|
||||
// Pass the new variable to the view
|
||||
return view('admin.invoice_edit', compact('invoice', 'shipment', 'customerTotalDue'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// UPDATE INVOICE
|
||||
// UPDATE INVOICE (HEADER LEVEL)
|
||||
// -------------------------------------------------------------
|
||||
// -------------------------------------------------------------
|
||||
// UPDATE INVOICE (HEADER LEVEL)
|
||||
// -------------------------------------------------------------
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
Log::info("🟡 Invoice Update Request Received", [
|
||||
Log::info('🟡 Invoice Update Request Received', [
|
||||
'invoice_id' => $id,
|
||||
'request' => $request->all()
|
||||
'request' => $request->all(),
|
||||
]);
|
||||
|
||||
$invoice = Invoice::findOrFail($id);
|
||||
|
||||
// 1) VALIDATION
|
||||
$data = $request->validate([
|
||||
'invoice_date' => 'required|date',
|
||||
'due_date' => 'required|date|after_or_equal:invoice_date',
|
||||
@@ -68,31 +79,32 @@ class AdminInvoiceController extends Controller
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
|
||||
Log::info("✅ Validated Invoice Update Data", $data);
|
||||
Log::info('✅ Validated Invoice Update Data', $data);
|
||||
|
||||
$finalAmount = floatval($data['final_amount']);
|
||||
$taxPercent = floatval($data['tax_percent']);
|
||||
$taxAmount = 0;
|
||||
// 2) CALCULATE GST / TOTALS
|
||||
$finalAmount = (float) $data['final_amount'];
|
||||
$taxPercent = (float) $data['tax_percent'];
|
||||
|
||||
if ($data['tax_type'] === 'gst') {
|
||||
Log::info("🟢 GST Selected", compact('taxPercent'));
|
||||
Log::info('🟢 GST Selected', compact('taxPercent'));
|
||||
|
||||
$data['cgst_percent'] = $taxPercent / 2;
|
||||
$data['sgst_percent'] = $taxPercent / 2;
|
||||
$data['igst_percent'] = 0;
|
||||
} else {
|
||||
Log::info("🔵 IGST Selected", compact('taxPercent'));
|
||||
Log::info('🔵 IGST Selected', compact('taxPercent'));
|
||||
|
||||
$data['cgst_percent'] = 0;
|
||||
$data['sgst_percent'] = 0;
|
||||
$data['igst_percent'] = $taxPercent;
|
||||
}
|
||||
|
||||
$taxAmount = ($finalAmount * $taxPercent) / 100;
|
||||
|
||||
$data['gst_amount'] = $taxAmount;
|
||||
$data['final_amount_with_gst'] = $finalAmount + $taxAmount;
|
||||
$gstAmount = ($finalAmount * $taxPercent) / 100;
|
||||
$data['gst_amount'] = $gstAmount;
|
||||
$data['final_amount_with_gst'] = $finalAmount + $gstAmount;
|
||||
$data['gst_percent'] = $taxPercent;
|
||||
|
||||
Log::info("📌 Final Calculated Invoice Values", [
|
||||
Log::info('📌 Final Calculated Invoice Values', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'final_amount' => $finalAmount,
|
||||
'gst_amount' => $data['gst_amount'],
|
||||
@@ -103,13 +115,28 @@ class AdminInvoiceController extends Controller
|
||||
'igst_percent' => $data['igst_percent'],
|
||||
]);
|
||||
|
||||
// 3) UPDATE DB
|
||||
$invoice->update($data);
|
||||
|
||||
Log::info("✅ Invoice Updated Successfully", [
|
||||
'invoice_id' => $invoice->id
|
||||
Log::info('✅ Invoice Updated Successfully', [
|
||||
'invoice_id' => $invoice->id,
|
||||
]);
|
||||
|
||||
// regenerate PDF
|
||||
// 4) LOG ACTUAL DB VALUES
|
||||
$invoice->refresh();
|
||||
Log::info('🔍 Invoice AFTER UPDATE (DB values)', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'final_amount' => $invoice->final_amount,
|
||||
'gst_percent' => $invoice->gst_percent,
|
||||
'gst_amount' => $invoice->gst_amount,
|
||||
'final_amount_with_gst' => $invoice->final_amount_with_gst,
|
||||
'tax_type' => $invoice->tax_type,
|
||||
'cgst_percent' => $invoice->cgst_percent,
|
||||
'sgst_percent' => $invoice->sgst_percent,
|
||||
'igst_percent' => $invoice->igst_percent,
|
||||
]);
|
||||
|
||||
// 5) REGENERATE PDF
|
||||
$this->generateInvoicePDF($invoice);
|
||||
|
||||
return redirect()
|
||||
@@ -117,13 +144,89 @@ class AdminInvoiceController extends Controller
|
||||
->with('success', 'Invoice updated & PDF generated successfully.');
|
||||
}
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 🔹 UPDATE INVOICE ITEMS (price + ttl_amount)
|
||||
// -------------------------------------------------------------
|
||||
public function updateItems(Request $request, Invoice $invoice)
|
||||
{
|
||||
Log::info('🟡 Invoice Items Update Request', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'payload' => $request->all(),
|
||||
]);
|
||||
|
||||
$data = $request->validate([
|
||||
'items' => ['required', 'array'],
|
||||
'items.*.price' => ['required', 'numeric', 'min:0'],
|
||||
'items.*.ttl_amount' => ['required', 'numeric', 'min:0'],
|
||||
]);
|
||||
|
||||
$itemsInput = $data['items'];
|
||||
|
||||
foreach ($itemsInput as $itemId => $itemData) {
|
||||
$item = InvoiceItem::where('id', $itemId)
|
||||
->where('invoice_id', $invoice->id)
|
||||
->first();
|
||||
|
||||
if (!$item) {
|
||||
Log::warning('Invoice item not found or mismatched invoice', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'item_id' => $itemId,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$item->price = $itemData['price'];
|
||||
$item->ttl_amount = $itemData['ttl_amount'];
|
||||
$item->save();
|
||||
}
|
||||
|
||||
$newBaseAmount = InvoiceItem::where('invoice_id', $invoice->id)
|
||||
->sum('ttl_amount');
|
||||
|
||||
$taxType = $invoice->tax_type;
|
||||
$cgstPercent = (float) ($invoice->cgst_percent ?? 0);
|
||||
$sgstPercent = (float) ($invoice->sgst_percent ?? 0);
|
||||
$igstPercent = (float) ($invoice->igst_percent ?? 0);
|
||||
|
||||
$gstPercent = 0;
|
||||
if ($taxType === 'gst') {
|
||||
$gstPercent = $cgstPercent + $sgstPercent;
|
||||
} elseif ($taxType === 'igst') {
|
||||
$gstPercent = $igstPercent;
|
||||
}
|
||||
|
||||
$gstAmount = $newBaseAmount * $gstPercent / 100;
|
||||
$finalWithGst = $newBaseAmount + $gstAmount;
|
||||
|
||||
$invoice->final_amount = $newBaseAmount;
|
||||
$invoice->gst_amount = $gstAmount;
|
||||
$invoice->final_amount_with_gst = $finalWithGst;
|
||||
$invoice->gst_percent = $gstPercent;
|
||||
$invoice->save();
|
||||
|
||||
Log::info('✅ Invoice items updated & totals recalculated', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'final_amount' => $invoice->final_amount,
|
||||
'gst_amount' => $invoice->gst_amount,
|
||||
'final_amount_with_gst' => $invoice->final_amount_with_gst,
|
||||
'tax_type' => $invoice->tax_type,
|
||||
'cgst_percent' => $invoice->cgst_percent,
|
||||
'sgst_percent' => $invoice->sgst_percent,
|
||||
'igst_percent' => $invoice->igst_percent,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Invoice items updated successfully.');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PDF GENERATION USING mPDF
|
||||
// -------------------------------------------------------------
|
||||
public function generateInvoicePDF($invoice)
|
||||
{
|
||||
$invoice->load(['items', 'order.shipments']);
|
||||
$shipment = $invoice->order?->shipments?->first();
|
||||
$invoice->load(['items', 'customer', 'container']);
|
||||
$shipment = null;
|
||||
|
||||
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
|
||||
$folder = public_path('invoices/');
|
||||
|
||||
@@ -132,14 +235,25 @@ class AdminInvoiceController extends Controller
|
||||
}
|
||||
|
||||
$filePath = $folder . $fileName;
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
$mpdf = new Mpdf(['mode' => 'utf-8', 'format' => 'A4', 'default_font' => 'sans-serif']);
|
||||
$html = view('admin.pdf.invoice', ['invoice' => $invoice, 'shipment' => $shipment])->render();
|
||||
$mpdf = new Mpdf([
|
||||
'mode' => 'utf-8',
|
||||
'format' => 'A4',
|
||||
'default_font' => 'sans-serif',
|
||||
]);
|
||||
|
||||
$html = view('admin.pdf.invoice', [
|
||||
'invoice' => $invoice,
|
||||
'shipment' => $shipment,
|
||||
])->render();
|
||||
|
||||
$mpdf->WriteHTML($html);
|
||||
$mpdf->Output($filePath, 'F');
|
||||
|
||||
$invoice->update(['pdf_path' => 'invoices/' . $fileName]);
|
||||
}
|
||||
|
||||
@@ -147,20 +261,15 @@ class AdminInvoiceController extends Controller
|
||||
{
|
||||
$invoice = Invoice::findOrFail($id);
|
||||
|
||||
// Generate PDF if missing
|
||||
if (
|
||||
!$invoice->pdf_path ||
|
||||
!file_exists(public_path($invoice->pdf_path))
|
||||
) {
|
||||
// ALWAYS regenerate to reflect latest HTML/CSS
|
||||
$this->generateInvoicePDF($invoice);
|
||||
$invoice->refresh();
|
||||
}
|
||||
|
||||
return response()->download(public_path($invoice->pdf_path));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INSTALLMENTS (ADD/DELETE)
|
||||
// INSTALLMENTS (ADD)
|
||||
// -------------------------------------------------------------
|
||||
public function storeInstallment(Request $request, $invoice_id)
|
||||
{
|
||||
@@ -172,15 +281,13 @@ class AdminInvoiceController extends Controller
|
||||
]);
|
||||
|
||||
$invoice = Invoice::findOrFail($invoice_id);
|
||||
|
||||
$paidTotal = $invoice->installments()->sum('amount');
|
||||
// Use GST-inclusive total for all calculations/checks
|
||||
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
||||
|
||||
if ($request->amount > $remaining) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Installment amount exceeds remaining balance.'
|
||||
'message' => 'Installment amount exceeds remaining balance.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
@@ -194,7 +301,6 @@ class AdminInvoiceController extends Controller
|
||||
|
||||
$newPaid = $paidTotal + $request->amount;
|
||||
|
||||
// Mark as 'paid' if GST-inclusive total is cleared
|
||||
if ($newPaid >= $invoice->final_amount_with_gst) {
|
||||
$invoice->update(['status' => 'paid']);
|
||||
|
||||
@@ -210,9 +316,13 @@ class AdminInvoiceController extends Controller
|
||||
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
||||
'baseAmount' => $invoice->final_amount,
|
||||
'remaining' => max(0, $invoice->final_amount_with_gst - $newPaid),
|
||||
'isCompleted' => $newPaid >= $invoice->final_amount_with_gst
|
||||
'isCompleted' => $newPaid >= $invoice->final_amount_with_gst,
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INSTALLMENTS (DELETE)
|
||||
// -------------------------------------------------------------
|
||||
public function deleteInstallment($id)
|
||||
{
|
||||
$installment = InvoiceInstallment::findOrFail($id);
|
||||
@@ -223,8 +333,7 @@ class AdminInvoiceController extends Controller
|
||||
$paidTotal = $invoice->installments()->sum('amount');
|
||||
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
||||
|
||||
// Update status if not fully paid anymore
|
||||
if ($remaining > 0 && $invoice->status === "paid") {
|
||||
if ($remaining > 0 && $invoice->status === 'paid') {
|
||||
$invoice->update(['status' => 'pending']);
|
||||
|
||||
$this->generateInvoicePDF($invoice);
|
||||
@@ -238,8 +347,7 @@ class AdminInvoiceController extends Controller
|
||||
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
||||
'baseAmount' => $invoice->final_amount,
|
||||
'remaining' => $remaining,
|
||||
'isZero' => $paidTotal == 0
|
||||
'isZero' => $paidTotal == 0,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -41,9 +41,52 @@ class AdminOrderController extends Controller
|
||||
return view('admin.orders_create', compact('markList'));
|
||||
}
|
||||
|
||||
/* ---------------------------
|
||||
* SHOW / POPUP
|
||||
* ---------------------------*/
|
||||
/**
|
||||
* Store a new order and optionally create initial invoice
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'mark_no' => 'required|string',
|
||||
'origin' => 'nullable|string',
|
||||
'destination' => 'nullable|string',
|
||||
// totals optional when creating without items
|
||||
'ctn' => 'nullable|numeric',
|
||||
'qty' => 'nullable|numeric',
|
||||
'ttl_qty' => 'nullable|numeric',
|
||||
'ttl_amount' => 'nullable|numeric',
|
||||
'cbm' => 'nullable|numeric',
|
||||
'ttl_cbm' => 'nullable|numeric',
|
||||
'kg' => 'nullable|numeric',
|
||||
'ttl_kg' => 'nullable|numeric',
|
||||
]);
|
||||
|
||||
$order = Order::create([
|
||||
'order_id' => $this->generateOrderId(),
|
||||
'mark_no' => $data['mark_no'],
|
||||
'origin' => $data['origin'] ?? null,
|
||||
'destination' => $data['destination'] ?? null,
|
||||
'ctn' => $data['ctn'] ?? 0,
|
||||
'qty' => $data['qty'] ?? 0,
|
||||
'ttl_qty' => $data['ttl_qty'] ?? 0,
|
||||
'ttl_amount' => $data['ttl_amount'] ?? 0,
|
||||
'cbm' => $data['cbm'] ?? 0,
|
||||
'ttl_cbm' => $data['ttl_cbm'] ?? 0,
|
||||
'kg' => $data['kg'] ?? 0,
|
||||
'ttl_kg' => $data['ttl_kg'] ?? 0,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
//If you want to auto-create an invoice at order creation, uncomment:
|
||||
// $this->createInvoice($order);
|
||||
|
||||
return redirect()->route('admin.orders.show', $order->id)
|
||||
->with('success', 'Order created successfully.');
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// SHOW / POPUP
|
||||
// ---------------------------
|
||||
public function show($id)
|
||||
{
|
||||
$order = Order::with('items', 'markList')->findOrFail($id);
|
||||
@@ -61,7 +104,15 @@ class AdminOrderController extends Controller
|
||||
$user = User::where('customer_id', $order->markList->customer_id)->first();
|
||||
}
|
||||
|
||||
return view('admin.popup', compact('order', 'user'));
|
||||
$data['order_id'] = $order->id;
|
||||
|
||||
OrderItem::create($data);
|
||||
|
||||
// recalc totals and save to order
|
||||
$this->recalcTotals($order);
|
||||
// $this->updateInvoiceFromOrder($order); // <-- NEW
|
||||
|
||||
return redirect()->back()->with('success', 'Item added and totals updated.');
|
||||
}
|
||||
|
||||
/* ---------------------------
|
||||
@@ -599,56 +650,78 @@ class AdminOrderController extends Controller
|
||||
// 7) invoice number
|
||||
$invoiceNumber = $this->generateInvoiceNumber();
|
||||
|
||||
// 8) customer fetch
|
||||
$markList = MarkList::where('mark_no', $order->mark_no)->first();
|
||||
$customer = null;
|
||||
if ($markList && $markList->customer_id) {
|
||||
$customer = User::where('customer_id', $markList->customer_id)->first();
|
||||
}
|
||||
// 1. Auto-generate invoice number
|
||||
// $lastInvoice = \App\Models\Invoice::latest()->first();
|
||||
// $nextInvoice = $lastInvoice ? $lastInvoice->id + 1 : 1;
|
||||
// $invoiceNumber = 'INV-' . date('Y') . '-' . str_pad($nextInvoice, 6, '0', STR_PAD_LEFT);
|
||||
|
||||
// 9) invoice create
|
||||
$invoice = Invoice::create([
|
||||
'order_id' => $order->id,
|
||||
'customer_id' => $customer->id ?? null,
|
||||
'mark_no' => $order->mark_no,
|
||||
'invoice_number' => $invoiceNumber,
|
||||
'invoice_date' => now(),
|
||||
'due_date' => now()->addDays(10),
|
||||
'payment_method' => null,
|
||||
'reference_no' => null,
|
||||
'status' => 'pending',
|
||||
'final_amount' => $total_amount,
|
||||
'gst_percent' => 0,
|
||||
'gst_amount' => 0,
|
||||
'final_amount_with_gst' => $total_amount,
|
||||
'customer_name' => $customer->customer_name ?? null,
|
||||
'company_name' => $customer->company_name ?? null,
|
||||
'customer_email' => $customer->email ?? null,
|
||||
'customer_mobile' => $customer->mobile_no ?? null,
|
||||
'customer_address' => $customer->address ?? null,
|
||||
'pincode' => $customer->pincode ?? null,
|
||||
'notes' => null,
|
||||
'pdf_path' => null,
|
||||
]);
|
||||
// 2. Fetch customer (using mark list → customer_id)
|
||||
// $markList = MarkList::where('mark_no', $order->mark_no)->first();
|
||||
// $customer = null;
|
||||
|
||||
// 10) invoice items
|
||||
foreach ($order->items as $item) {
|
||||
InvoiceItem::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
'description' => $item->description,
|
||||
'ctn' => $item->ctn,
|
||||
'qty' => $item->qty,
|
||||
'ttl_qty' => $item->ttl_qty,
|
||||
'unit' => $item->unit,
|
||||
'price' => $item->price,
|
||||
'ttl_amount' => $item->ttl_amount,
|
||||
'cbm' => $item->cbm,
|
||||
'ttl_cbm' => $item->ttl_cbm,
|
||||
'kg' => $item->kg,
|
||||
'ttl_kg' => $item->ttl_kg,
|
||||
'shop_no' => $item->shop_no,
|
||||
]);
|
||||
}
|
||||
// if ($markList && $markList->customer_id) {
|
||||
// $customer = \App\Models\User::where('customer_id', $markList->customer_id)->first();
|
||||
// }
|
||||
|
||||
// 3. Create Invoice Record
|
||||
// $invoice = \App\Models\Invoice::create([
|
||||
// 'order_id' => $order->id,
|
||||
// 'customer_id' => $customer->id ?? null,
|
||||
// 'mark_no' => $order->mark_no,
|
||||
|
||||
// 'invoice_number' => $invoiceNumber,
|
||||
// 'invoice_date' => now(),
|
||||
// 'due_date' => now()->addDays(10),
|
||||
|
||||
// 'payment_method' => null,
|
||||
// 'reference_no' => null,
|
||||
// 'status' => 'pending',
|
||||
|
||||
// 'final_amount' => $total_amount,
|
||||
// 'gst_percent' => 0,
|
||||
// 'gst_amount' => 0,
|
||||
// 'final_amount_with_gst' => $total_amount,
|
||||
|
||||
// // snapshot customer fields
|
||||
// 'customer_name' => $customer->customer_name ?? null,
|
||||
// 'company_name' => $customer->company_name ?? null,
|
||||
// 'customer_email' => $customer->email ?? null,
|
||||
// 'customer_mobile' => $customer->mobile_no ?? null,
|
||||
// 'customer_address' => $customer->address ?? null,
|
||||
// 'pincode' => $customer->pincode ?? null,
|
||||
|
||||
// 'notes' => null,
|
||||
// ]);
|
||||
|
||||
// 4. Clone order items into invoice_items
|
||||
// foreach ($order->items as $item) {
|
||||
// \App\Models\InvoiceItem::create([
|
||||
// 'invoice_id' => $invoice->id,
|
||||
// 'description' => $item->description,
|
||||
// 'ctn' => $item->ctn,
|
||||
// 'qty' => $item->qty,
|
||||
// 'ttl_qty' => $item->ttl_qty,
|
||||
// 'unit' => $item->unit,
|
||||
// 'price' => $item->price,
|
||||
// 'ttl_amount' => $item->ttl_amount,
|
||||
// 'cbm' => $item->cbm,
|
||||
// 'ttl_cbm' => $item->ttl_cbm,
|
||||
// 'kg' => $item->kg,
|
||||
// 'ttl_kg' => $item->ttl_kg,
|
||||
// 'shop_no' => $item->shop_no,
|
||||
// ]);
|
||||
// }
|
||||
|
||||
// 5. TODO: PDF generation (I will add this later)
|
||||
// $invoice->pdf_path = null; // placeholder for now
|
||||
// $invoice->save();
|
||||
|
||||
// =======================
|
||||
// END INVOICE CREATION
|
||||
// =======================
|
||||
|
||||
// CLEAR TEMP DATA
|
||||
session()->forget(['temp_order_items', 'mark_no', 'origin', 'destination']);
|
||||
|
||||
return redirect()->route('admin.orders.index')
|
||||
->with('success', 'Order + Invoice created successfully.');
|
||||
|
||||
577
app/Http/Controllers/ContainerController.php
Normal file
577
app/Http/Controllers/ContainerController.php
Normal file
@@ -0,0 +1,577 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Container;
|
||||
use App\Models\ContainerRow;
|
||||
use App\Models\MarkList;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use Illuminate\Http\Request;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
class ContainerController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$containers = Container::with('rows')->latest()->get();
|
||||
|
||||
$containers->each(function ($container) {
|
||||
$rows = $container->rows;
|
||||
|
||||
$totalCtn = 0;
|
||||
$totalQty = 0;
|
||||
$totalCbm = 0;
|
||||
$totalKg = 0;
|
||||
|
||||
$ctnKeys = ['CTN', 'CTNS'];
|
||||
$qtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY', 'QTY', 'PCS', 'PIECES'];
|
||||
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
|
||||
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
|
||||
|
||||
$getFirstNumeric = function (array $data, array $possibleKeys) {
|
||||
$normalizedMap = [];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key === null || $key === '') continue;
|
||||
|
||||
$normKey = strtoupper((string)$key);
|
||||
$normKey = str_replace([' ', '/', '-', '.'], '', $normKey);
|
||||
$normalizedMap[$normKey] = $value;
|
||||
}
|
||||
|
||||
foreach ($possibleKeys as $search) {
|
||||
$normSearch = strtoupper($search);
|
||||
$normSearch = str_replace([' ', '/', '-', '.'], '', $normSearch);
|
||||
|
||||
foreach ($normalizedMap as $nKey => $value) {
|
||||
if (
|
||||
strpos($nKey, $normSearch) !== false &&
|
||||
(is_numeric($value) || (is_string($value) && is_numeric(trim($value))))
|
||||
) {
|
||||
return (float) trim($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$data = $row->data ?? [];
|
||||
|
||||
$totalCtn += $getFirstNumeric($data, $ctnKeys);
|
||||
$totalQty += $getFirstNumeric($data, $qtyKeys);
|
||||
$totalCbm += $getFirstNumeric($data, $cbmKeys);
|
||||
$totalKg += $getFirstNumeric($data, $kgKeys);
|
||||
}
|
||||
|
||||
$container->summary = [
|
||||
'total_ctn' => round($totalCtn, 2),
|
||||
'total_qty' => round($totalQty, 2),
|
||||
'total_cbm' => round($totalCbm, 3),
|
||||
'total_kg' => round($totalKg, 2),
|
||||
];
|
||||
});
|
||||
|
||||
return view('admin.container', compact('containers'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('admin.container_create');
|
||||
}
|
||||
|
||||
private function isValidExcelFormat($rows, $header)
|
||||
{
|
||||
if (empty($header) || count($rows) < 2) return false;
|
||||
|
||||
$validKeywords = [
|
||||
'MARK', 'DESCRIPTION', 'DESC', 'CTN', 'CTNS', 'QTY', 'TOTALQTY', 'ITLQTY', 'ITL QTY',
|
||||
'UNIT', 'CBM', 'TOTAL CBM', 'KG', 'TOTAL KG',
|
||||
'METAL BUCKLE', 'WATCH MOVEMENT', 'STEEL BOTTLE',
|
||||
'MEHULPAID', 'ITEM NO', 'ITEM NO.', 'SAHILPAID', 'PINAKIN', 'GST',
|
||||
'MOON LAMP', 'TRANSPARENT BOTTLE', 'PLASTIC FONDANT',
|
||||
];
|
||||
|
||||
$headerText = implode(' ', array_map('strtoupper', $header));
|
||||
$requiredHeaders = ['CTN', 'QTY', 'DESCRIPTION', 'DESC'];
|
||||
|
||||
$hasValidHeaders = false;
|
||||
foreach ($requiredHeaders as $key) {
|
||||
if (stripos($headerText, $key) !== false) {
|
||||
$hasValidHeaders = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$hasValidHeaders) return false;
|
||||
|
||||
$dataPreview = '';
|
||||
for ($i = 0; $i < min(5, count($rows)); $i++) {
|
||||
$rowText = implode(' ', array_slice($rows[$i], 0, 10));
|
||||
$dataPreview .= ' ' . strtoupper((string)$rowText);
|
||||
}
|
||||
|
||||
$validMatches = 0;
|
||||
foreach ($validKeywords as $keyword) {
|
||||
if (stripos($headerText . $dataPreview, strtoupper($keyword)) !== false) {
|
||||
$validMatches++;
|
||||
}
|
||||
}
|
||||
|
||||
return $validMatches >= 3;
|
||||
}
|
||||
|
||||
private function normalizeKey($value): string
|
||||
{
|
||||
$norm = strtoupper((string)$value);
|
||||
return str_replace([' ', '/', '-', '.'], '', $norm);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'container_name' => 'required|string',
|
||||
'container_number' => 'required|string|unique:containers,container_number',
|
||||
'container_date' => 'required|date',
|
||||
'excel_file' => 'required|file|mimes:xls,xlsx',
|
||||
]);
|
||||
|
||||
$file = $request->file('excel_file');
|
||||
$sheets = Excel::toArray([], $file);
|
||||
$rows = $sheets[0] ?? [];
|
||||
|
||||
if (count($rows) < 2) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'Excel file is empty.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
// HEADER DETECTION
|
||||
$headerRowIndex = null;
|
||||
$header = [];
|
||||
|
||||
foreach ($rows as $i => $row) {
|
||||
$trimmed = array_map(fn($v) => trim((string)$v), $row);
|
||||
$nonEmpty = array_filter($trimmed, fn($v) => $v !== '');
|
||||
if (empty($nonEmpty)) continue;
|
||||
|
||||
if (count($nonEmpty) >= 4) {
|
||||
$headerRowIndex = $i;
|
||||
$header = $trimmed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($headerRowIndex === null) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'Header row not found in Excel.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
if (!$this->isValidExcelFormat($rows, $header)) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'Only MEHUL / SAHIL / PINAKIN / GST loading list formats allowed.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
// COLUMN INDEXES
|
||||
$essentialColumns = [
|
||||
'desc_col' => null,
|
||||
'ctn_col' => null,
|
||||
'qty_col' => null,
|
||||
'totalqty_col' => null,
|
||||
'unit_col' => null,
|
||||
'price_col' => null,
|
||||
'amount_col' => null,
|
||||
'cbm_col' => null,
|
||||
'totalcbm_col' => null,
|
||||
'kg_col' => null,
|
||||
'totalkg_col' => null,
|
||||
'itemno_col' => null,
|
||||
];
|
||||
|
||||
foreach ($header as $colIndex => $headingText) {
|
||||
if (empty($headingText)) continue;
|
||||
|
||||
$normalized = $this->normalizeKey($headingText);
|
||||
|
||||
if (strpos($normalized, 'DESCRIPTION') !== false || strpos($normalized, 'DESC') !== false) {
|
||||
$essentialColumns['desc_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'CTN') !== false || strpos($normalized, 'CTNS') !== false) {
|
||||
$essentialColumns['ctn_col'] = $colIndex;
|
||||
} elseif (
|
||||
strpos($normalized, 'ITLQTY') !== false ||
|
||||
strpos($normalized, 'TOTALQTY') !== false ||
|
||||
strpos($normalized, 'TTLQTY') !== false
|
||||
) {
|
||||
$essentialColumns['totalqty_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'QTY') !== false) {
|
||||
$essentialColumns['qty_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'UNIT') !== false) {
|
||||
$essentialColumns['unit_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'PRICE') !== false) {
|
||||
$essentialColumns['price_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'AMOUNT') !== false) {
|
||||
$essentialColumns['amount_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'TOTALCBM') !== false || strpos($normalized, 'ITLCBM') !== false) {
|
||||
$essentialColumns['totalcbm_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'CBM') !== false) {
|
||||
$essentialColumns['cbm_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'TOTALKG') !== false || strpos($normalized, 'TTKG') !== false) {
|
||||
$essentialColumns['totalkg_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'KG') !== false) {
|
||||
$essentialColumns['kg_col'] = $colIndex;
|
||||
} elseif (
|
||||
strpos($normalized, 'MARKNO') !== false ||
|
||||
strpos($normalized, 'MARK') !== false ||
|
||||
strpos($normalized, 'ITEMNO') !== false ||
|
||||
strpos($normalized, 'ITEM') !== false
|
||||
) {
|
||||
$essentialColumns['itemno_col'] = $colIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($essentialColumns['itemno_col'])) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'Mark / Item column not found in Excel (expected headers like MARK NO / Mark_No / Item_No).'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
// ROWS CLEANING
|
||||
$dataRows = array_slice($rows, $headerRowIndex + 1);
|
||||
$cleanedRows = [];
|
||||
$unmatchedRowsData = [];
|
||||
|
||||
foreach ($dataRows as $offset => $row) {
|
||||
$trimmedRow = array_map(fn($v) => trim((string)$v), $row);
|
||||
$nonEmptyCells = array_filter($trimmedRow, fn($v) => $v !== '');
|
||||
if (count($nonEmptyCells) < 2) continue;
|
||||
|
||||
$rowText = strtoupper(implode(' ', $trimmedRow));
|
||||
if (
|
||||
stripos($rowText, 'TOTAL') !== false ||
|
||||
stripos($rowText, 'TTL') !== false ||
|
||||
stripos($rowText, 'GRAND') !== false
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$descValue = '';
|
||||
if ($essentialColumns['desc_col'] !== null) {
|
||||
$descValue = trim($row[$essentialColumns['desc_col']] ?? '');
|
||||
}
|
||||
|
||||
if ($essentialColumns['desc_col'] !== null && $descValue === '' && count($nonEmptyCells) >= 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cleanedRows[] = [
|
||||
'row' => $row,
|
||||
'offset' => $offset,
|
||||
];
|
||||
}
|
||||
|
||||
// MARK CHECK: strict - collect ALL marks + unmatched rows
|
||||
$marksFromExcel = [];
|
||||
foreach ($cleanedRows as $item) {
|
||||
$row = $item['row'];
|
||||
$rawMark = $row[$essentialColumns['itemno_col']] ?? null;
|
||||
$mark = trim((string)($rawMark ?? ''));
|
||||
if ($mark !== '') {
|
||||
$marksFromExcel[] = $mark;
|
||||
}
|
||||
}
|
||||
|
||||
$marksFromExcel = array_values(array_unique($marksFromExcel));
|
||||
|
||||
if (empty($marksFromExcel)) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'No mark numbers found in Excel file.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
$validMarks = MarkList::whereIn('mark_no', $marksFromExcel)
|
||||
->where('status', 'active')
|
||||
->pluck('mark_no')
|
||||
->toArray();
|
||||
|
||||
$unmatchedMarks = array_values(array_diff($marksFromExcel, $validMarks));
|
||||
|
||||
if (!empty($unmatchedMarks)) {
|
||||
foreach ($cleanedRows as $item) {
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
$rowMark = trim((string)($row[$essentialColumns['itemno_col']] ?? ''));
|
||||
|
||||
if ($rowMark === '' || !in_array($rowMark, $unmatchedMarks)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rowData = [];
|
||||
foreach ($header as $colIndex => $headingText) {
|
||||
$value = $row[$colIndex] ?? null;
|
||||
if (is_string($value)) $value = trim($value);
|
||||
$rowData[$headingText] = $value;
|
||||
}
|
||||
|
||||
$unmatchedRowsData[] = [
|
||||
'excel_row' => $headerRowIndex + 1 + $offset,
|
||||
'mark_no' => $rowMark,
|
||||
'data' => $rowData,
|
||||
];
|
||||
}
|
||||
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'Some mark numbers are not found in Mark List. Container not created.'])
|
||||
->withInput()
|
||||
->with('unmatched_rows', $unmatchedRowsData);
|
||||
}
|
||||
|
||||
// STEP 1: Marks → customers mapping + grouping
|
||||
|
||||
$markRecords = MarkList::whereIn('mark_no', $marksFromExcel)
|
||||
->where('status', 'active')
|
||||
->get();
|
||||
|
||||
$markToCustomerId = [];
|
||||
$markToSnapshot = [];
|
||||
|
||||
foreach ($markRecords as $mr) {
|
||||
$markToCustomerId[$mr->mark_no] = $mr->customer_id;
|
||||
|
||||
$markToSnapshot[$mr->mark_no] = [
|
||||
'customer_name' => $mr->customer_name,
|
||||
'company_name' => $mr->company_name,
|
||||
'mobile_no' => $mr->mobile_no,
|
||||
];
|
||||
}
|
||||
|
||||
$groupedByCustomer = [];
|
||||
|
||||
foreach ($cleanedRows as $item) {
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
|
||||
$rawMark = $row[$essentialColumns['itemno_col']] ?? null;
|
||||
$mark = trim((string)($rawMark ?? ''));
|
||||
|
||||
if ($mark === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$customerId = $markToCustomerId[$mark] ?? null;
|
||||
if (!$customerId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($groupedByCustomer[$customerId])) {
|
||||
$groupedByCustomer[$customerId] = [];
|
||||
}
|
||||
|
||||
$groupedByCustomer[$customerId][] = [
|
||||
'row' => $row,
|
||||
'offset' => $offset,
|
||||
'mark' => $mark,
|
||||
];
|
||||
}
|
||||
|
||||
// STEP 2: Container + ContainerRows save
|
||||
|
||||
$container = Container::create([
|
||||
'container_name' => $request->container_name,
|
||||
'container_number' => $request->container_number,
|
||||
'container_date' => $request->container_date,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
$path = $file->store('containers');
|
||||
$container->update(['excel_file' => $path]);
|
||||
|
||||
$savedCount = 0;
|
||||
|
||||
foreach ($cleanedRows as $item) {
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
|
||||
$data = [];
|
||||
foreach ($header as $colIndex => $headingText) {
|
||||
$value = $row[$colIndex] ?? null;
|
||||
if (is_string($value)) $value = trim($value);
|
||||
$data[$headingText] = $value;
|
||||
}
|
||||
|
||||
ContainerRow::create([
|
||||
'container_id' => $container->id,
|
||||
'row_index' => $headerRowIndex + 1 + $offset,
|
||||
'data' => $data,
|
||||
]);
|
||||
|
||||
$savedCount++;
|
||||
}
|
||||
|
||||
// STEP 3: per-customer invoices + invoice items
|
||||
|
||||
$invoiceCount = 0;
|
||||
|
||||
foreach ($groupedByCustomer as $customerId => $rowsForCustomer) {
|
||||
if (empty($rowsForCustomer)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$firstMark = $rowsForCustomer[0]['mark'];
|
||||
$snap = $markToSnapshot[$firstMark] ?? null;
|
||||
|
||||
$invoice = new Invoice();
|
||||
$invoice->container_id = $container->id;
|
||||
// $invoice->customer_id = $customerId;
|
||||
$invoice->invoice_number = $this->generateInvoiceNumber();
|
||||
$invoice->invoice_date = now()->toDateString();
|
||||
$invoice->due_date = null;
|
||||
|
||||
if ($snap) {
|
||||
$invoice->customer_name = $snap['customer_name'] ?? null;
|
||||
$invoice->company_name = $snap['company_name'] ?? null;
|
||||
$invoice->customer_mobile = $snap['mobile_no'] ?? null;
|
||||
}
|
||||
|
||||
$invoice->final_amount = 0;
|
||||
$invoice->gst_percent = 0;
|
||||
$invoice->gst_amount = 0;
|
||||
$invoice->final_amount_with_gst = 0;
|
||||
|
||||
$invoice->customer_email = null;
|
||||
$invoice->customer_address = null;
|
||||
$invoice->pincode = null;
|
||||
|
||||
$uniqueMarks = array_unique(array_column($rowsForCustomer, 'mark'));
|
||||
$invoice->notes = 'Auto-created from Container ' . $container->container_number
|
||||
. ' for Mark(s): ' . implode(', ', $uniqueMarks);
|
||||
|
||||
$invoice->pdf_path = null;
|
||||
$invoice->status = 'pending';
|
||||
|
||||
$invoice->save();
|
||||
$invoiceCount++;
|
||||
|
||||
$totalAmount = 0;
|
||||
|
||||
foreach ($rowsForCustomer as $item) {
|
||||
$row = $item['row'];
|
||||
|
||||
$description = $essentialColumns['desc_col'] !== null ? ($row[$essentialColumns['desc_col']] ?? null) : null;
|
||||
$ctn = $essentialColumns['ctn_col'] !== null ? (int)($row[$essentialColumns['ctn_col']] ?? 0) : 0;
|
||||
$qty = $essentialColumns['qty_col'] !== null ? (int)($row[$essentialColumns['qty_col']] ?? 0) : 0;
|
||||
$ttlQty = $essentialColumns['totalqty_col'] !== null ? (int)($row[$essentialColumns['totalqty_col']] ?? 0) : $qty;
|
||||
$unit = $essentialColumns['unit_col'] !== null ? ($row[$essentialColumns['unit_col']] ?? null) : null;
|
||||
$price = $essentialColumns['price_col'] !== null ? (float)($row[$essentialColumns['price_col']] ?? 0) : 0;
|
||||
$ttlAmount = $essentialColumns['amount_col'] !== null ? (float)($row[$essentialColumns['amount_col']] ?? 0) : 0;
|
||||
$cbm = $essentialColumns['cbm_col'] !== null ? (float)($row[$essentialColumns['cbm_col']] ?? 0) : 0;
|
||||
$ttlCbm = $essentialColumns['totalcbm_col'] !== null ? (float)($row[$essentialColumns['totalcbm_col']] ?? $cbm) : $cbm;
|
||||
$kg = $essentialColumns['kg_col'] !== null ? (float)($row[$essentialColumns['kg_col']] ?? 0) : 0;
|
||||
$ttlKg = $essentialColumns['totalkg_col'] !== null ? (float)($row[$essentialColumns['totalkg_col']] ?? $kg) : $kg;
|
||||
|
||||
InvoiceItem::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
'description'=> $description,
|
||||
'ctn' => $ctn,
|
||||
'qty' => $qty,
|
||||
'ttl_qty' => $ttlQty,
|
||||
'unit' => $unit,
|
||||
'price' => $price,
|
||||
'ttl_amount' => $ttlAmount,
|
||||
'cbm' => $cbm,
|
||||
'ttl_cbm' => $ttlCbm,
|
||||
'kg' => $kg,
|
||||
'ttl_kg' => $ttlKg,
|
||||
'shop_no' => null,
|
||||
]);
|
||||
|
||||
$totalAmount += $ttlAmount;
|
||||
}
|
||||
|
||||
$invoice->final_amount = $totalAmount;
|
||||
$invoice->gst_percent = 0;
|
||||
$invoice->gst_amount = 0;
|
||||
$invoice->final_amount_with_gst = $totalAmount;
|
||||
|
||||
$invoice->save();
|
||||
}
|
||||
|
||||
$msg = "Container '{$container->container_number}' created with {$savedCount} rows and {$invoiceCount} customer invoice(s).";
|
||||
return redirect()->route('containers.index')->with('success', $msg);
|
||||
}
|
||||
|
||||
public function show(Container $container)
|
||||
{
|
||||
$container->load('rows');
|
||||
return view('admin.container_show', compact('container'));
|
||||
}
|
||||
|
||||
public function updateRows(Request $request, Container $container)
|
||||
{
|
||||
$rowsInput = $request->input('rows', []);
|
||||
|
||||
foreach ($rowsInput as $rowId => $cols) {
|
||||
$row = ContainerRow::where('container_id', $container->id)
|
||||
->where('id', $rowId)
|
||||
->first();
|
||||
|
||||
if (!$row) continue;
|
||||
|
||||
$data = $row->data ?? [];
|
||||
|
||||
foreach ($cols as $colHeader => $value) {
|
||||
$data[$colHeader] = $value;
|
||||
}
|
||||
|
||||
$row->update([
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('containers.show', $container->id)
|
||||
->with('success', 'Excel rows updated successfully.');
|
||||
}
|
||||
|
||||
public function updateStatus(Request $request, Container $container)
|
||||
{
|
||||
$request->validate(['status' => 'required|in:pending,in-progress,completed,cancelled']);
|
||||
|
||||
$container->update(['status' => $request->status]);
|
||||
|
||||
return redirect()->route('containers.index')->with('success', 'Status updated.');
|
||||
}
|
||||
|
||||
public function destroy(Container $container)
|
||||
{
|
||||
$container->delete();
|
||||
return redirect()->route('containers.index')->with('success', 'Container deleted.');
|
||||
}
|
||||
|
||||
private function generateInvoiceNumber(): string
|
||||
{
|
||||
$year = now()->format('Y');
|
||||
|
||||
$last = Invoice::whereYear('created_at', $year)
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
if ($last) {
|
||||
$parts = explode('-', $last->invoice_number);
|
||||
$seq = 0;
|
||||
|
||||
if (count($parts) === 3) {
|
||||
$seq = (int) $parts[2];
|
||||
}
|
||||
|
||||
$nextSeq = $seq + 1;
|
||||
} else {
|
||||
$nextSeq = 1;
|
||||
}
|
||||
|
||||
return 'INV-' . $year . '-' . str_pad($nextSeq, 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
}
|
||||
@@ -67,9 +67,12 @@ class ChatController extends Controller
|
||||
'sender_id' => auth()->id(),
|
||||
'sender_type' => \App\Models\User::class,
|
||||
'message' => $request->message,
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
'client_id' => $request->client_id, // ✅ ADD
|
||||
'read_by_admin' => false,
|
||||
'read_by_user' => true,
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
];
|
||||
|
||||
// Handle file upload
|
||||
@@ -86,7 +89,11 @@ class ChatController extends Controller
|
||||
$message->load('sender');
|
||||
|
||||
// Fire real-time event
|
||||
<<<<<<< HEAD
|
||||
broadcast(new NewChatMessage($message))->toOthers();
|
||||
=======
|
||||
broadcast(new NewChatMessage($message));
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
|
||||
@@ -10,6 +10,15 @@ class ChatMessage extends Model
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
<<<<<<< HEAD
|
||||
'ticket_id',
|
||||
'sender_id',
|
||||
'sender_type', // user OR admin
|
||||
'message',
|
||||
'file_path',
|
||||
'file_type',
|
||||
];
|
||||
=======
|
||||
'ticket_id',
|
||||
'sender_id',
|
||||
'sender_type',
|
||||
@@ -21,6 +30,7 @@ class ChatMessage extends Model
|
||||
'client_id',
|
||||
];
|
||||
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
|
||||
/**
|
||||
* The ticket this message belongs to.
|
||||
|
||||
30
app/Models/Container.php
Normal file
30
app/Models/Container.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Container extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'container_name',
|
||||
'container_number',
|
||||
'container_date',
|
||||
'status',
|
||||
'excel_file',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'container_date' => 'date',
|
||||
];
|
||||
|
||||
public function rows()
|
||||
{
|
||||
return $this->hasMany(ContainerRow::class);
|
||||
}
|
||||
|
||||
public function invoices()
|
||||
{
|
||||
return $this->hasMany(Invoice::class);
|
||||
}
|
||||
}
|
||||
23
app/Models/ContainerRow.php
Normal file
23
app/Models/ContainerRow.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ContainerRow extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'container_id',
|
||||
'row_index',
|
||||
'data',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'data' => 'array',
|
||||
];
|
||||
|
||||
public function container()
|
||||
{
|
||||
return $this->belongsTo(Container::class);
|
||||
}
|
||||
}
|
||||
@@ -9,41 +9,30 @@ class Invoice extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
|
||||
protected $fillable = [
|
||||
'order_id',
|
||||
'container_id',
|
||||
'customer_id',
|
||||
'mark_no',
|
||||
|
||||
'invoice_number',
|
||||
'invoice_date',
|
||||
'due_date',
|
||||
|
||||
'payment_method',
|
||||
'reference_no',
|
||||
'status',
|
||||
|
||||
'final_amount', // without tax
|
||||
|
||||
'tax_type', // gst / igst
|
||||
'gst_percent', // only used for gst UI input
|
||||
'cgst_percent',
|
||||
'sgst_percent',
|
||||
'igst_percent',
|
||||
|
||||
'gst_amount', // total tax amount
|
||||
'final_amount',
|
||||
'gst_percent',
|
||||
'gst_amount',
|
||||
'final_amount_with_gst',
|
||||
|
||||
'customer_name',
|
||||
'company_name',
|
||||
'customer_email',
|
||||
'customer_mobile',
|
||||
'customer_address',
|
||||
'pincode',
|
||||
|
||||
'pdf_path',
|
||||
'notes',
|
||||
];
|
||||
|
||||
];
|
||||
|
||||
/****************************
|
||||
* Relationships
|
||||
@@ -54,16 +43,28 @@ class Invoice extends Model
|
||||
return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC');
|
||||
}
|
||||
|
||||
public function order()
|
||||
// NEW: invoice आता container वर depend
|
||||
public function container()
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
return $this->belongsTo(Container::class);
|
||||
}
|
||||
|
||||
// OLD: order() relation काढले आहे
|
||||
// public function order()
|
||||
// {
|
||||
// return $this->belongsTo(Order::class);
|
||||
// }
|
||||
|
||||
public function customer()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'customer_id');
|
||||
}
|
||||
|
||||
public function installments()
|
||||
{
|
||||
return $this->hasMany(InvoiceInstallment::class);
|
||||
}
|
||||
|
||||
/****************************
|
||||
* Helper Functions
|
||||
****************************/
|
||||
@@ -82,30 +83,10 @@ class Invoice extends Model
|
||||
return $this->status === 'pending' && now()->gt($this->due_date);
|
||||
}
|
||||
|
||||
// जर पुढे container → shipment relation असेल तर हा helper नंतर adjust करू
|
||||
public function getShipment()
|
||||
{
|
||||
return $this->order?->shipments?->first();
|
||||
// आधी order वरून shipment घेत होत; container flow मध्ये नंतर गरज पडल्यास बदलू
|
||||
return null;
|
||||
}
|
||||
|
||||
public function installments()
|
||||
{
|
||||
return $this->hasMany(InvoiceInstallment::class);
|
||||
}
|
||||
|
||||
// App\Models\Invoice.php
|
||||
|
||||
public function totalPaid()
|
||||
{
|
||||
return $this->installments()->sum('amount');
|
||||
}
|
||||
|
||||
public function remainingAmount()
|
||||
{
|
||||
return max(
|
||||
($this->final_amount_with_gst ?? 0) - $this->totalPaid(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
28
app/Models/LoadingListItem.php
Normal file
28
app/Models/LoadingListItem.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class LoadingListItem extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'container_id',
|
||||
'mark',
|
||||
'description',
|
||||
'ctn',
|
||||
'qty',
|
||||
'total_qty',
|
||||
'unit',
|
||||
'price',
|
||||
'cbm',
|
||||
'total_cbm',
|
||||
'kg',
|
||||
'total_kg',
|
||||
];
|
||||
|
||||
public function container()
|
||||
{
|
||||
return $this->belongsTo(Container::class);
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,8 @@
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"platform-check": false
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
|
||||
65
composer.lock
generated
65
composer.lock
generated
@@ -2880,6 +2880,18 @@
|
||||
},
|
||||
{
|
||||
"name": "maennchen/zipstream-php",
|
||||
<<<<<<< HEAD
|
||||
"version": "3.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maennchen/ZipStream-PHP.git",
|
||||
"reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/682f1098a8fddbaf43edac2306a691c7ad508ec5",
|
||||
"reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5",
|
||||
=======
|
||||
"version": "3.1.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
@@ -2890,21 +2902,34 @@
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f",
|
||||
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f",
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"ext-zlib": "*",
|
||||
<<<<<<< HEAD
|
||||
"php-64bit": "^8.3"
|
||||
=======
|
||||
"php-64bit": "^8.2"
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
},
|
||||
"require-dev": {
|
||||
"brianium/paratest": "^7.7",
|
||||
"ext-zip": "*",
|
||||
<<<<<<< HEAD
|
||||
"friendsofphp/php-cs-fixer": "^3.86",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"php-coveralls/php-coveralls": "^2.5",
|
||||
"phpunit/phpunit": "^12.0",
|
||||
=======
|
||||
"friendsofphp/php-cs-fixer": "^3.16",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"php-coveralls/php-coveralls": "^2.5",
|
||||
"phpunit/phpunit": "^11.0",
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
"vimeo/psalm": "^6.0"
|
||||
},
|
||||
"suggest": {
|
||||
@@ -2946,7 +2971,11 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
|
||||
<<<<<<< HEAD
|
||||
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.1"
|
||||
=======
|
||||
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2"
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2954,7 +2983,11 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
<<<<<<< HEAD
|
||||
"time": "2025-12-10T09:58:31+00:00"
|
||||
=======
|
||||
"time": "2025-01-27T12:07:53+00:00"
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
},
|
||||
{
|
||||
"name": "markbaker/complex",
|
||||
@@ -4181,6 +4214,18 @@
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
<<<<<<< HEAD
|
||||
"version": "1.30.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||
"reference": "09cdde5e2f078b9a3358dd217e2c8cb4dac84be2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/09cdde5e2f078b9a3358dd217e2c8cb4dac84be2",
|
||||
"reference": "09cdde5e2f078b9a3358dd217e2c8cb4dac84be2",
|
||||
=======
|
||||
"version": "1.30.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
@@ -4191,6 +4236,7 @@
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fa8257a579ec623473eabfe49731de5967306c4c",
|
||||
"reference": "fa8257a579ec623473eabfe49731de5967306c4c",
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4213,12 +4259,19 @@
|
||||
"markbaker/complex": "^3.0",
|
||||
"markbaker/matrix": "^3.0",
|
||||
"php": ">=7.4.0 <8.5.0",
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
|
||||
<<<<<<< HEAD
|
||||
"doctrine/instantiator": "^1.5",
|
||||
=======
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
"dompdf/dompdf": "^1.0 || ^2.0 || ^3.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.2",
|
||||
"mitoteam/jpgraph": "^10.3",
|
||||
@@ -4265,6 +4318,12 @@
|
||||
},
|
||||
{
|
||||
"name": "Adrien Crivelli"
|
||||
<<<<<<< HEAD
|
||||
},
|
||||
{
|
||||
"name": "Owen Leibman"
|
||||
=======
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
}
|
||||
],
|
||||
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
|
||||
@@ -4281,9 +4340,15 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
||||
<<<<<<< HEAD
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.2"
|
||||
},
|
||||
"time": "2026-01-11T05:58:24+00:00"
|
||||
=======
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.1"
|
||||
},
|
||||
"time": "2025-10-26T16:01:04+00:00"
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
|
||||
@@ -6,11 +6,6 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
@@ -19,11 +14,6 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. Set this in your ".env" file.
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
@@ -32,11 +22,6 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
@@ -45,11 +30,6 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| the application so that it's available within Artisan commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
@@ -58,11 +38,6 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
@@ -71,11 +46,6 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
@@ -88,11 +58,6 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
@@ -109,13 +74,6 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
@@ -123,4 +81,53 @@ return [
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Class Aliases
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'aliases' => [
|
||||
|
||||
'App' => Illuminate\Support\Facades\App::class,
|
||||
'Arr' => Illuminate\Support\Arr::class,
|
||||
'Artisan' => Illuminate\Support\Facades\Artisan::class,
|
||||
'Auth' => Illuminate\Support\Facades\Auth::class,
|
||||
'Blade' => Illuminate\Support\Facades\Blade::class,
|
||||
'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
|
||||
'Bus' => Illuminate\Support\Facades\Bus::class,
|
||||
'Cache' => Illuminate\Support\Facades\Cache::class,
|
||||
'Config' => Illuminate\Support\Facades\Config::class,
|
||||
'Cookie' => Illuminate\Support\Facades\Cookie::class,
|
||||
'Crypt' => Illuminate\Support\Facades\Crypt::class,
|
||||
'DB' => Illuminate\Support\Facades\DB::class,
|
||||
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
|
||||
'Event' => Illuminate\Support\Facades\Event::class,
|
||||
'File' => Illuminate\Support\Facades\File::class,
|
||||
'Gate' => Illuminate\Support\Facades\Gate::class,
|
||||
'Hash' => Illuminate\Support\Facades\Hash::class,
|
||||
'Http' => Illuminate\Support\Facades\Http::class,
|
||||
'Lang' => Illuminate\Support\Facades\Lang::class,
|
||||
'Log' => Illuminate\Support\Facades\Log::class,
|
||||
'Mail' => Illuminate\Support\Facades\Mail::class,
|
||||
'Notification' => Illuminate\Support\Facades\Notification::class,
|
||||
'Password' => Illuminate\Support\Facades\Password::class,
|
||||
'Queue' => Illuminate\Support\Facades\Queue::class,
|
||||
'RateLimiter' => Illuminate\Support\Facades\RateLimiter::class,
|
||||
'Redirect' => Illuminate\Support\Facades\Redirect::class,
|
||||
'Request' => Illuminate\Support\Facades\Request::class,
|
||||
'Response' => Illuminate\Support\Facades\Response::class,
|
||||
'Route' => Illuminate\Support\Facades\Route::class,
|
||||
'Schema' => Illuminate\Support\Facades\Schema::class,
|
||||
'Session' => Illuminate\Support\Facades\Session::class,
|
||||
'Storage' => Illuminate\Support\Facades\Storage::class,
|
||||
'Str' => Illuminate\Support\Str::class,
|
||||
'URL' => Illuminate\Support\Facades\URL::class,
|
||||
'Validator' => Illuminate\Support\Facades\Validator::class,
|
||||
'View' => Illuminate\Support\Facades\View::class,
|
||||
|
||||
// ✅ Laravel‑Excel facade
|
||||
'Excel' => Maatwebsite\Excel\Facades\Excel::class,
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
380
config/excel.php
Normal file
380
config/excel.php
Normal file
@@ -0,0 +1,380 @@
|
||||
<?php
|
||||
|
||||
use Maatwebsite\Excel\Excel;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Csv;
|
||||
|
||||
return [
|
||||
'exports' => [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Chunk size
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using FromQuery, the query is automatically chunked.
|
||||
| Here you can specify how big the chunk should be.
|
||||
|
|
||||
*/
|
||||
'chunk_size' => 1000,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pre-calculate formulas during export
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'pre_calculate_formulas' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Enable strict null comparison
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When enabling strict null comparison empty cells ('') will
|
||||
| be added to the sheet.
|
||||
*/
|
||||
'strict_null_comparison' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| CSV Settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configure e.g. delimiter, enclosure and line ending for CSV exports.
|
||||
|
|
||||
*/
|
||||
'csv' => [
|
||||
'delimiter' => ',',
|
||||
'enclosure' => '"',
|
||||
'line_ending' => PHP_EOL,
|
||||
'use_bom' => false,
|
||||
'include_separator_line' => false,
|
||||
'excel_compatibility' => false,
|
||||
'output_encoding' => '',
|
||||
'test_auto_detect' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Worksheet properties
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configure e.g. default title, creator, subject,...
|
||||
|
|
||||
*/
|
||||
'properties' => [
|
||||
'creator' => '',
|
||||
'lastModifiedBy' => '',
|
||||
'title' => '',
|
||||
'description' => '',
|
||||
'subject' => '',
|
||||
'keywords' => '',
|
||||
'category' => '',
|
||||
'manager' => '',
|
||||
'company' => '',
|
||||
],
|
||||
],
|
||||
|
||||
'imports' => [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Read Only
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When dealing with imports, you might only be interested in the
|
||||
| data that the sheet exists. By default we ignore all styles,
|
||||
| however if you want to do some logic based on style data
|
||||
| you can enable it by setting read_only to false.
|
||||
|
|
||||
*/
|
||||
'read_only' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Ignore Empty
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When dealing with imports, you might be interested in ignoring
|
||||
| rows that have null values or empty strings. By default rows
|
||||
| containing empty strings or empty values are not ignored but can be
|
||||
| ignored by enabling the setting ignore_empty to true.
|
||||
|
|
||||
*/
|
||||
'ignore_empty' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Heading Row Formatter
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configure the heading row formatter.
|
||||
| Available options: none|slug|custom
|
||||
|
|
||||
*/
|
||||
'heading_row' => [
|
||||
'formatter' => 'slug',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| CSV Settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configure e.g. delimiter, enclosure and line ending for CSV imports.
|
||||
|
|
||||
*/
|
||||
'csv' => [
|
||||
'delimiter' => null,
|
||||
'enclosure' => '"',
|
||||
'escape_character' => '\\',
|
||||
'contiguous' => false,
|
||||
'input_encoding' => Csv::GUESS_ENCODING,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Worksheet properties
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configure e.g. default title, creator, subject,...
|
||||
|
|
||||
*/
|
||||
'properties' => [
|
||||
'creator' => '',
|
||||
'lastModifiedBy' => '',
|
||||
'title' => '',
|
||||
'description' => '',
|
||||
'subject' => '',
|
||||
'keywords' => '',
|
||||
'category' => '',
|
||||
'manager' => '',
|
||||
'company' => '',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cell Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configure middleware that is executed on getting a cell value
|
||||
|
|
||||
*/
|
||||
'cells' => [
|
||||
'middleware' => [
|
||||
//\Maatwebsite\Excel\Middleware\TrimCellValue::class,
|
||||
//\Maatwebsite\Excel\Middleware\ConvertEmptyCellValuesToNull::class,
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Extension detector
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configure here which writer/reader type should be used when the package
|
||||
| needs to guess the correct type based on the extension alone.
|
||||
|
|
||||
*/
|
||||
'extension_detector' => [
|
||||
'xlsx' => Excel::XLSX,
|
||||
'xlsm' => Excel::XLSX,
|
||||
'xltx' => Excel::XLSX,
|
||||
'xltm' => Excel::XLSX,
|
||||
'xls' => Excel::XLS,
|
||||
'xlt' => Excel::XLS,
|
||||
'ods' => Excel::ODS,
|
||||
'ots' => Excel::ODS,
|
||||
'slk' => Excel::SLK,
|
||||
'xml' => Excel::XML,
|
||||
'gnumeric' => Excel::GNUMERIC,
|
||||
'htm' => Excel::HTML,
|
||||
'html' => Excel::HTML,
|
||||
'csv' => Excel::CSV,
|
||||
'tsv' => Excel::TSV,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| PDF Extension
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configure here which Pdf driver should be used by default.
|
||||
| Available options: Excel::MPDF | Excel::TCPDF | Excel::DOMPDF
|
||||
|
|
||||
*/
|
||||
'pdf' => Excel::DOMPDF,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Value Binder
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| PhpSpreadsheet offers a way to hook into the process of a value being
|
||||
| written to a cell. In there some assumptions are made on how the
|
||||
| value should be formatted. If you want to change those defaults,
|
||||
| you can implement your own default value binder.
|
||||
|
|
||||
| Possible value binders:
|
||||
|
|
||||
| [x] Maatwebsite\Excel\DefaultValueBinder::class
|
||||
| [x] PhpOffice\PhpSpreadsheet\Cell\StringValueBinder::class
|
||||
| [x] PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder::class
|
||||
|
|
||||
*/
|
||||
'value_binder' => [
|
||||
'default' => Maatwebsite\Excel\DefaultValueBinder::class,
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default cell caching driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By default PhpSpreadsheet keeps all cell values in memory, however when
|
||||
| dealing with large files, this might result into memory issues. If you
|
||||
| want to mitigate that, you can configure a cell caching driver here.
|
||||
| When using the illuminate driver, it will store each value in the
|
||||
| cache store. This can slow down the process, because it needs to
|
||||
| store each value. You can use the "batch" store if you want to
|
||||
| only persist to the store when the memory limit is reached.
|
||||
|
|
||||
| Drivers: memory|illuminate|batch
|
||||
|
|
||||
*/
|
||||
'driver' => 'memory',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Batch memory caching
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When dealing with the "batch" caching driver, it will only
|
||||
| persist to the store when the memory limit is reached.
|
||||
| Here you can tweak the memory limit to your liking.
|
||||
|
|
||||
*/
|
||||
'batch' => [
|
||||
'memory_limit' => 60000,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Illuminate cache
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "illuminate" caching driver, it will automatically use
|
||||
| your default cache store. However if you prefer to have the cell
|
||||
| cache on a separate store, you can configure the store name here.
|
||||
| You can use any store defined in your cache config. When leaving
|
||||
| at "null" it will use the default store.
|
||||
|
|
||||
*/
|
||||
'illuminate' => [
|
||||
'store' => null,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Time-to-live (TTL)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The TTL of items written to cache. If you want to keep the items cached
|
||||
| indefinitely, set this to null. Otherwise, set a number of seconds,
|
||||
| a \DateInterval, or a callable.
|
||||
|
|
||||
| Allowable types: callable|\DateInterval|int|null
|
||||
|
|
||||
*/
|
||||
'default_ttl' => 10800,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Transaction Handler
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By default the import is wrapped in a transaction. This is useful
|
||||
| for when an import may fail and you want to retry it. With the
|
||||
| transactions, the previous import gets rolled-back.
|
||||
|
|
||||
| You can disable the transaction handler by setting this to null.
|
||||
| Or you can choose a custom made transaction handler here.
|
||||
|
|
||||
| Supported handlers: null|db
|
||||
|
|
||||
*/
|
||||
'transactions' => [
|
||||
'handler' => 'db',
|
||||
'db' => [
|
||||
'connection' => null,
|
||||
],
|
||||
],
|
||||
|
||||
'temporary_files' => [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Local Temporary Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When exporting and importing files, we use a temporary file, before
|
||||
| storing reading or downloading. Here you can customize that path.
|
||||
| permissions is an array with the permission flags for the directory (dir)
|
||||
| and the create file (file).
|
||||
|
|
||||
*/
|
||||
'local_path' => storage_path('framework/cache/laravel-excel'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Local Temporary Path Permissions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Permissions is an array with the permission flags for the directory (dir)
|
||||
| and the create file (file).
|
||||
| If omitted the default permissions of the filesystem will be used.
|
||||
|
|
||||
*/
|
||||
'local_permissions' => [
|
||||
// 'dir' => 0755,
|
||||
// 'file' => 0644,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Remote Temporary Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When dealing with a multi server setup with queues in which you
|
||||
| cannot rely on having a shared local temporary path, you might
|
||||
| want to store the temporary file on a shared disk. During the
|
||||
| queue executing, we'll retrieve the temporary file from that
|
||||
| location instead. When left to null, it will always use
|
||||
| the local path. This setting only has effect when using
|
||||
| in conjunction with queued imports and exports.
|
||||
|
|
||||
*/
|
||||
'remote_disk' => null,
|
||||
'remote_prefix' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Force Resync
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When dealing with a multi server setup as above, it's possible
|
||||
| for the clean up that occurs after entire queue has been run to only
|
||||
| cleanup the server that the last AfterImportJob runs on. The rest of the server
|
||||
| would still have the local temporary file stored on it. In this case your
|
||||
| local storage limits can be exceeded and future imports won't be processed.
|
||||
| To mitigate this you can set this config value to be true, so that after every
|
||||
| queued chunk is processed the local temporary file is deleted on the server that
|
||||
| processed it.
|
||||
|
|
||||
*/
|
||||
'force_resync_remote' => null,
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('containers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('container_name');
|
||||
$table->string('container_number')->unique();
|
||||
$table->date('container_date');
|
||||
$table->string('excel_file')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('containers');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('loading_list_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('container_id')
|
||||
->constrained('containers')
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->string('mark')->nullable(); // MARK / ITEM NO
|
||||
$table->string('description')->nullable();
|
||||
$table->integer('ctn')->nullable();
|
||||
$table->integer('qty')->nullable();
|
||||
$table->integer('total_qty')->nullable();
|
||||
$table->string('unit')->nullable();
|
||||
$table->decimal('price', 15, 3)->nullable(); // SAHIL format साठी
|
||||
$table->decimal('cbm', 15, 5)->nullable();
|
||||
$table->decimal('total_cbm', 15, 5)->nullable();
|
||||
$table->decimal('kg', 15, 3)->nullable();
|
||||
$table->decimal('total_kg', 15, 3)->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('loading_list_items');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('container_rows', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('container_id')
|
||||
->constrained('containers')
|
||||
->onDelete('cascade');
|
||||
|
||||
// Excel मधल्या row क्रमांकासाठी (optional)
|
||||
$table->unsignedInteger('row_index')->nullable();
|
||||
|
||||
// या row चा full data: "heading text" => "cell value"
|
||||
$table->json('data');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('container_rows');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('containers', function (Blueprint $table) {
|
||||
$table->string('status', 20)
|
||||
->default('pending')
|
||||
->after('container_date');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('containers', function (Blueprint $table) {
|
||||
$table->dropColumn('status');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
// 1) order_id foreign key काढा
|
||||
$table->dropForeign(['order_id']);
|
||||
|
||||
// 2) order_id column काढा
|
||||
$table->dropColumn('order_id');
|
||||
|
||||
// 3) container_id add करा
|
||||
$table->unsignedBigInteger('container_id')->nullable()->after('id');
|
||||
|
||||
// 4) container_id FK
|
||||
$table->foreign('container_id')
|
||||
->references('id')
|
||||
->on('containers')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
// rollback: container_id काढून order_id परत add
|
||||
$table->dropForeign(['container_id']);
|
||||
$table->dropColumn('container_id');
|
||||
|
||||
$table->unsignedBigInteger('order_id')->index();
|
||||
$table->foreign('order_id')
|
||||
->references('id')
|
||||
->on('orders')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
};
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,30 +1,31 @@
|
||||
import Echo from "laravel-echo";
|
||||
import Pusher from "pusher-js";
|
||||
import Echo from 'laravel-echo';
|
||||
import Pusher from 'pusher-js';
|
||||
|
||||
window.Pusher = Pusher;
|
||||
|
||||
console.log("[ECHO] Initializing Reverb...");
|
||||
|
||||
// Get CSRF token from meta tag
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
window.Echo = new Echo({
|
||||
broadcaster: "reverb",
|
||||
broadcaster: 'reverb',
|
||||
key: import.meta.env.VITE_REVERB_APP_KEY,
|
||||
|
||||
wsHost: import.meta.env.VITE_REVERB_HOST,
|
||||
wsPort: Number(import.meta.env.VITE_REVERB_PORT),
|
||||
wsPort: import.meta.env.VITE_REVERB_PORT ?? 8080,
|
||||
wssPort: import.meta.env.VITE_REVERB_PORT ?? 8080,
|
||||
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'http') === 'https',
|
||||
enabledTransports: ['ws', 'wss'],
|
||||
|
||||
forceTLS: false,
|
||||
disableStats: true,
|
||||
authEndpoint: '/admin/broadcasting/auth',
|
||||
|
||||
authEndpoint: "/broadcasting/auth",
|
||||
|
||||
// ⭐ MOST IMPORTANT ⭐
|
||||
withCredentials: true,
|
||||
|
||||
auth: {
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": document
|
||||
.querySelector('meta[name="csrf-token"]')
|
||||
?.getAttribute("content"),
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
},
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[ECHO] Loaded Successfully!", window.Echo);
|
||||
console.log('%c[ECHO] Initialized!', 'color: green; font-weight: bold;', window.Echo);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<style>
|
||||
/* ---------- Base ---------- */
|
||||
|
||||
:root{
|
||||
--primary-1:#1a2951;
|
||||
--primary-2:#243a72;
|
||||
@@ -37,7 +38,7 @@ body {
|
||||
/* header */
|
||||
.account-header {
|
||||
margin-bottom: 18px;
|
||||
background: linear-gradient(90deg,var(--primary-1),var(--primary-2));
|
||||
background:linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 22px 26px;
|
||||
border-radius: var(--rounded);
|
||||
box-shadow: 0 6px 18px rgba(34,50,90,0.12);
|
||||
@@ -62,7 +63,7 @@ body {
|
||||
|
||||
.btn {
|
||||
display:inline-flex; align-items:center; justify-content:center; gap:8px;
|
||||
background: linear-gradient(90deg,var(--primary-1),var(--primary-2));
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color:#fff; border:none; padding:10px 16px; border-radius:10px; font-weight:600;
|
||||
cursor:pointer; transition: transform .15s ease, box-shadow .15s;
|
||||
}
|
||||
@@ -334,7 +335,7 @@ tr:hover td{ background:#fbfdff; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
/* margin-right:-550px; */
|
||||
margin-right:-550px;
|
||||
|
||||
}
|
||||
|
||||
@@ -342,7 +343,7 @@ tr:hover td{ background:#fbfdff; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-right:-550px;
|
||||
margin-right:8=500px;
|
||||
|
||||
}
|
||||
|
||||
@@ -458,7 +459,7 @@ tr:hover td{ background:#fbfdff; }
|
||||
filter: brightness(0) saturate(100%) invert(84%) sepia(8%) saturate(165%) hue-rotate(179deg) brightness(89%) contrast(86%);
|
||||
}
|
||||
|
||||
/* ---------- Entry Details Modal (existing) ---------- */
|
||||
/* ---------- Entry Details Modal (Enhanced) ---------- */
|
||||
.modal-fade1 {
|
||||
position:fixed;
|
||||
inset:0;
|
||||
@@ -480,6 +481,9 @@ tr:hover td{ background:#fbfdff; }
|
||||
max-height:92vh;
|
||||
overflow:auto;
|
||||
min-height: 500px;
|
||||
}
|
||||
#entryOrdersModal {
|
||||
z-index: 1300;
|
||||
}
|
||||
#entryOrdersModal {
|
||||
z-index: 1300;
|
||||
@@ -489,7 +493,7 @@ tr:hover td{ background:#fbfdff; }
|
||||
.entry-summary-cards {
|
||||
display:flex;
|
||||
gap:16px;
|
||||
margin-bottom:20px;
|
||||
margin-top:25px;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
.entry-summary-card {
|
||||
@@ -504,6 +508,184 @@ tr:hover td{ background:#fbfdff; }
|
||||
.entry-summary-label{ font-size:12px; color:var(--muted); }
|
||||
.entry-summary-value{ font-size:18px; font-weight:700; color:#253047; margin-top:6px; }
|
||||
|
||||
/* Enhanced dropdown */
|
||||
.installment-status-dropdown {
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
padding: 10px 40px 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1.5px solid #e3eaf6;
|
||||
background: white;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%236b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.installment-status-dropdown:hover {
|
||||
border-color: #c2d1f0;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.installment-status-dropdown:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(39, 109, 234, 0.1);
|
||||
}
|
||||
|
||||
/* Status-specific dropdown options */
|
||||
.installment-status-dropdown option {
|
||||
padding: 12px !important;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.installment-status-dropdown option[value="Pending"] {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.installment-status-dropdown option[value="Loading"] {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.installment-status-dropdown option[value="Packed"] {
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.installment-status-dropdown option[value="Dispatched"] {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.installment-status-dropdown option[value="Delivered"] {
|
||||
color: #0c6b2e;
|
||||
}
|
||||
|
||||
/* ---------- Entry Orders Modal (Enhanced) ---------- */
|
||||
.entry-orders-modal .modal-box1 {
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(18, 30, 60, 0.25);
|
||||
}
|
||||
|
||||
.entry-orders-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 24px 30px;
|
||||
color: white;
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
.entry-orders-header h2 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.entry-orders-header .subtitle {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.entry-orders-content {
|
||||
padding: 30px;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Orders table in modal */
|
||||
.orders-table-container {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #eef3fb;
|
||||
border-radius: 12px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.orders-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.orders-table th {
|
||||
background: linear-gradient(90deg, #f8fbff, #f5f9ff);
|
||||
padding: 14px 16px;
|
||||
text-align: left;
|
||||
font-weight: 700;
|
||||
color: var(--primary-1);
|
||||
border-bottom: 2px solid #eef3fb;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.orders-table td {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #f1f6ff;
|
||||
}
|
||||
|
||||
.orders-table tr:hover {
|
||||
background: #fbfdff;
|
||||
}
|
||||
|
||||
.orders-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Order ID with badge style */
|
||||
.order-id-badge {
|
||||
display: inline-block;
|
||||
background: linear-gradient(90deg, #f0f7ff, #e6f0ff);
|
||||
color: var(--primary-1);
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
border: 1px solid #dbe4f5;
|
||||
}
|
||||
|
||||
/* Summary row */
|
||||
.orders-summary-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
background: linear-gradient(90deg, #e6ebf5 0%, #f9fbff 100%);
|
||||
|
||||
|
||||
border-radius: 10px;
|
||||
margin-top: 20px;
|
||||
border: 1px solid #eef3fb;
|
||||
}
|
||||
|
||||
.orders-summary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.orders-summary-value {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-1);
|
||||
}
|
||||
|
||||
.orders-summary-label {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin-top: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* installment modal */
|
||||
#installmentModal .modal-box1 { max-width:720px; min-width:380px; }
|
||||
|
||||
@@ -828,7 +1010,10 @@ tr:hover td{ background:#fbfdff; }
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
background: linear-gradient(90deg, #f9fbff, #f7faff);
|
||||
background:linear-gradient(90deg, #e6ebf5 0%, #f9fbff 100%);
|
||||
|
||||
|
||||
|
||||
border-radius: 8px;
|
||||
border: 1px solid #eef3fb;
|
||||
}
|
||||
@@ -1110,6 +1295,40 @@ tr:hover td{ background:#fbfdff; }
|
||||
.pagination-container {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
/* Responsive modals */
|
||||
|
||||
|
||||
.entry-details-modal .modal-box1,
|
||||
.entry-orders-modal .modal-box1 {
|
||||
margin: 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.entry-details-header,
|
||||
.entry-orders-header {
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.entry-details-content,
|
||||
.entry-orders-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.entry-summary-cards {
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.entry-summary-value {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.orders-summary-row {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Zoom out specific fix */
|
||||
@@ -1146,6 +1365,10 @@ tr:hover td{ background:#fbfdff; }
|
||||
.pagination-info {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.entry-summary-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Prevent horizontal scroll on very small screens */
|
||||
@@ -1178,6 +1401,7 @@ html, body {
|
||||
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<div class="account-container">
|
||||
@@ -1323,7 +1547,7 @@ html, body {
|
||||
<!-- CREATE ORDER POPUP MODAL -->
|
||||
<div class="create-order-modal" id="createOrderModal">
|
||||
<div class="modal-box">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:16px;">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:16px; background:linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding:20px; border-radius:12px; color:white; margin-top:-20px; margin-left:-20px; margin-right:-15px; margin-top:-15px;">
|
||||
<div style="font-size:20px; font-weight:800;">Create New Installment</div>
|
||||
<button class="btn ghost" id="closeCreateModal" title="Close create form">✕</button>
|
||||
</div>
|
||||
@@ -1415,37 +1639,64 @@ html, body {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ENTRY DETAILS MODAL -->
|
||||
<div class="modal-fade1" id="entryDetailsModal">
|
||||
<div class="modal-box1 entry-details-modal">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||
<div>
|
||||
<h2 style="margin:0;font-size:20px;color:#223256;font-weight:800">Entry Details — <span id="entryDetailsId">-</span></h2>
|
||||
<div style="font-size:13px;color:var(--muted)">Complete view of all installments for this entry.</div>
|
||||
<!-- ENTRY DETAILS MODAL (Enhanced) -->
|
||||
<div class="modal-fade1 entry-details-modal" id="entryDetailsModal">
|
||||
<div class="modal-box1">
|
||||
<div class="entry-details-header" >
|
||||
<h2 >Entry Details</h2>
|
||||
<div class="subtitle">
|
||||
<span id="entryDetailsId">Loading...</span>
|
||||
<span>• Complete view of all installments for this entry</span>
|
||||
</div>
|
||||
<div><button class="btn ghost" onclick="closeEntryDetailsModal()">Close</button></div>
|
||||
</div>
|
||||
|
||||
<div class="entry-details-content">
|
||||
<div class="entry-summary-cards">
|
||||
<div class="entry-summary-card">
|
||||
<div class="entry-summary-label">Original Amount</div>
|
||||
<div class="entry-summary-label">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
|
||||
</svg>
|
||||
Original Amount
|
||||
</div>
|
||||
<div class="entry-summary-value" id="originalAmount">-</div>
|
||||
</div>
|
||||
<div class="entry-summary-card">
|
||||
<div class="entry-summary-label">Total Processed</div>
|
||||
<div class="entry-summary-label">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
|
||||
</svg>
|
||||
Total Processed
|
||||
</div>
|
||||
<div class="entry-summary-value" id="totalProcessed">-</div>
|
||||
</div>
|
||||
<div class="entry-summary-card">
|
||||
<div class="entry-summary-label">Pending Balance</div>
|
||||
<div class="entry-summary-label">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>
|
||||
</svg>
|
||||
Pending Balance
|
||||
</div>
|
||||
<div class="entry-summary-value" id="pendingBalance">-</div>
|
||||
</div>
|
||||
<div class="entry-summary-card">
|
||||
<div class="entry-summary-label">Total Installments</div>
|
||||
<div class="entry-summary-label">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
Total Installments
|
||||
</div>
|
||||
<div class="entry-summary-value" id="totalInstallments">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="entry-installments-table" style="width:100%; border-collapse:collapse;">
|
||||
<div style="margin-top: 30px;">
|
||||
<h3 style="font-size: 18px; font-weight: 700; color: var(--primary-1); margin-bottom: 16px;">
|
||||
Installment History
|
||||
</h3>
|
||||
|
||||
<div class="orders-table-container">
|
||||
<table class="entry-installments-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Installment</th>
|
||||
@@ -1460,36 +1711,40 @@ html, body {
|
||||
<tr><td colspan="6" class="empty-state">No installments yet</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; justify-content: flex-end; gap:12px; margin-top:16px;">
|
||||
<button type="button" class="btn ghost" onclick="closeEntryDetailsModal()">Close</button>
|
||||
<div style="padding: 20px 30px; border-top: 1px solid #eef3fb; display: flex; justify-content: space-between; align-items: center;">
|
||||
<button type="button" class="btn ghost" onclick="closeEntryDetailsModal()">
|
||||
Close
|
||||
</button>
|
||||
@can('account.add_installment')
|
||||
<button type="button" class="btn" id="addInstallmentFromDetails">+ Add New Installment</button>
|
||||
<button type="button" class="btn" id="addInstallmentFromDetails">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 6px;">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
Add New Installment
|
||||
</button>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ENTRY ORDERS MODAL -->
|
||||
<div class="modal-fade1" id="entryOrdersModal">
|
||||
<div class="modal-box1" style="max-width: 1000px;">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
|
||||
<div>
|
||||
<h2 style="margin:0;font-size:20px;color:#223256;font-weight:800;">
|
||||
Entry Orders
|
||||
<span id="entryOrdersEntryNo-span" style="font-size:14px;color:#6b7280;margin-left:8px;"></span>
|
||||
</h2>
|
||||
<div style="font-size:13px;color:#6f7b8f;">
|
||||
All orders associated with this entry.
|
||||
<!-- ENTRY ORDERS MODAL (Enhanced) -->
|
||||
<div class="modal-fade1 entry-orders-modal" id="entryOrdersModal">
|
||||
<div class="modal-box1">
|
||||
<div class="entry-orders-header">
|
||||
<h2>Entry Orders</h2>
|
||||
<div class="subtitle">
|
||||
<span id="entryOrdersEntryNo-span">Loading...</span>
|
||||
<span>• All orders associated with this entry</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn ghost" type="button" onclick="closeEntryOrdersModal()">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="entry-orders-content">
|
||||
<div class="orders-table-container">
|
||||
<table class="orders-table" style="width:100%;border-collapse:collapse;font-size:13px;">
|
||||
<table class="orders-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order ID</th>
|
||||
@@ -1503,23 +1758,40 @@ html, body {
|
||||
</thead>
|
||||
<tbody id="entryOrdersTableBody">
|
||||
<tr>
|
||||
<td colspan="7" class="empty-state">No orders associated with this entry</td>
|
||||
<td colspan="7" class="empty-state">Loading orders...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="orders-summary-row">
|
||||
<div class="orders-summary-item">
|
||||
<div class="orders-summary-value" id="ordersTotalCount">0</div>
|
||||
<div class="orders-summary-label">Total Orders</div>
|
||||
</div>
|
||||
<div class="orders-summary-item">
|
||||
<div class="orders-summary-value" id="ordersTotalQuantity">0</div>
|
||||
<div class="orders-summary-label">Total Quantity</div>
|
||||
</div>
|
||||
<div class="orders-summary-item">
|
||||
<div class="orders-summary-value" id="ordersTotalAmount">₹0</div>
|
||||
<div class="orders-summary-label">Total Amount</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 20px 30px; border-top: 1px solid #eef3fb; display: flex; justify-content: flex-end;">
|
||||
<button class="btn ghost" type="button" onclick="closeEntryOrdersModal()">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Installment Modal -->
|
||||
<div class="modal-fade1" id="installmentModal">
|
||||
<div class="modal-box1" style="max-width:720px;">
|
||||
<div style="display:flex;align-items:center; justify-content:space-between; margin-bottom:12px;">
|
||||
<div style="display:flex;align-items:center; justify-content:space-between; margin-bottom:12px; background:linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding:16px; border-radius:8px; color:white; margin-top:-15px; margin-left:-20px; margin-right:-15px;">
|
||||
<div style="font-size:18px;font-weight:800;color:#243a72;">+ Add New Installment</div>
|
||||
<button class="btn ghost" onclick="closeInstallmentModal()">✕</button>
|
||||
</div>
|
||||
@@ -2784,9 +3056,16 @@ async function submitEditEntry(e) {
|
||||
}
|
||||
|
||||
|
||||
function openEntryOrdersModal(entryNo) {
|
||||
function openEntryOrdersModal(entryNo) {
|
||||
// Set entry number in header
|
||||
document.getElementById('entryOrdersEntryNo-span').textContent = `(${entryNo})`;
|
||||
|
||||
// Reset summary values
|
||||
document.getElementById('ordersTotalCount').textContent = '0';
|
||||
document.getElementById('ordersTotalQuantity').textContent = '0';
|
||||
document.getElementById('ordersTotalAmount').textContent = '₹0';
|
||||
|
||||
// table loading state
|
||||
const tbody = document.getElementById('entryOrdersTableBody');
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
@@ -2794,6 +3073,7 @@ async function submitEditEntry(e) {
|
||||
</tr>
|
||||
`;
|
||||
|
||||
// API call: /admin/account/entry-orders/{entryno}
|
||||
jsonFetch(`/admin/account/entry-orders/${encodeURIComponent(entryNo)}`, {
|
||||
method: 'GET'
|
||||
})
|
||||
@@ -2819,29 +3099,44 @@ async function submitEditEntry(e) {
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
let totalQuantity = 0;
|
||||
let totalAmount = 0;
|
||||
|
||||
orders.forEach(order => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
const amountValue =
|
||||
order.ttl_amount ??
|
||||
order.ttlamount ??
|
||||
order.total_amount ??
|
||||
order.order_amount ??
|
||||
order.amount ??
|
||||
0;
|
||||
const idString = (order.orderid ?? order.id ?? '').toString().trim();
|
||||
const numericId = parseInt(idString, 10);
|
||||
const formattedId = isNaN(numericId)
|
||||
? escapeHtml(idString)
|
||||
: 'KNT-25-' + String(numericId).padStart(8, '0');
|
||||
|
||||
// Calculate values for summary
|
||||
const quantity = Number(order.qty || order.order_qty || 0);
|
||||
const amountValue = Number(order.ttl_amount ?? order.ttlamount ?? order.total_amount ?? order.order_amount ?? order.amount ?? 0);
|
||||
|
||||
totalQuantity += quantity;
|
||||
totalAmount += amountValue;
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>${escapeHtml(order.order_id)}</td>
|
||||
<td>${escapeHtml(order.mark_no ?? '')}</td>
|
||||
<td>
|
||||
<span class="order-id-badge">${formattedId}</span>
|
||||
</td>
|
||||
<td>${escapeHtml(order.markno ?? order.mark_no ?? '')}</td>
|
||||
<td>${escapeHtml(order.origin ?? '')}</td>
|
||||
<td>${escapeHtml(order.destination ?? '')}</td>
|
||||
<td>${escapeHtml(order.ctn ?? '')}</td>
|
||||
<td>${escapeHtml(order.qty ?? '')}</td>
|
||||
<td>${formatCurrency(amountValue)}</td>
|
||||
<td><strong>${quantity}</strong></td>
|
||||
<td><strong>${formatCurrency(amountValue)}</strong></td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// Update summary
|
||||
document.getElementById('ordersTotalCount').textContent = orders.length;
|
||||
document.getElementById('ordersTotalQuantity').textContent = totalQuantity.toLocaleString();
|
||||
document.getElementById('ordersTotalAmount').textContent = formatCurrency(totalAmount);
|
||||
|
||||
})
|
||||
.catch(() => {
|
||||
tbody.innerHTML = `
|
||||
@@ -2852,8 +3147,7 @@ async function submitEditEntry(e) {
|
||||
});
|
||||
|
||||
document.getElementById('entryOrdersModal').classList.add('modal-open');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function closeEntryOrdersModal() {
|
||||
document.getElementById('entryOrdersModal').classList.remove('modal-open');
|
||||
@@ -2989,7 +3283,7 @@ function handleSearch(){
|
||||
updatePaginationControls();
|
||||
}
|
||||
|
||||
/* ---------- Entry details & installments ---------- */
|
||||
/* ---------- Entry details & installments (Enhanced) ---------- */
|
||||
async function openEntryDetailsModal(entryNo) {
|
||||
try {
|
||||
const res = await jsonFetch('/admin/account/entry/' + encodeURIComponent(entryNo));
|
||||
@@ -2999,58 +3293,109 @@ async function openEntryDetailsModal(entryNo) {
|
||||
const entry = res.entry;
|
||||
currentEntry = entry;
|
||||
|
||||
// Set header info
|
||||
document.getElementById('entryDetailsId').textContent = entry.entry_no;
|
||||
document.getElementById('originalAmount').textContent = formatCurrency(entry.amount);
|
||||
|
||||
const totalProcessed = Number(entry.amount) - Number(entry.pending_amount);
|
||||
// Calculate values
|
||||
const originalAmount = Number(entry.amount || 0);
|
||||
const pendingAmount = Number(entry.pending_amount || 0);
|
||||
const totalProcessed = originalAmount - pendingAmount;
|
||||
|
||||
// Update summary cards
|
||||
document.getElementById('originalAmount').textContent = formatCurrency(originalAmount);
|
||||
document.getElementById('totalProcessed').textContent = formatCurrency(totalProcessed);
|
||||
document.getElementById('pendingBalance').textContent = formatCurrency(pendingAmount);
|
||||
document.getElementById('totalInstallments').textContent = entry.installments?.length || 0;
|
||||
|
||||
document.getElementById('pendingBalance').textContent = formatCurrency(entry.pending_amount);
|
||||
document.getElementById('totalInstallments').textContent = entry.installments.length;
|
||||
|
||||
// Render installments table
|
||||
const tbody = document.getElementById('installmentsTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (!entry.installments || entry.installments.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="empty-state">
|
||||
<div style="padding: 30px; text-align: center;">
|
||||
<div style="font-size: 48px; color: #eef3fb; margin-bottom: 16px;">📋</div>
|
||||
<div style="font-size: 16px; color: #6f7b8f; font-weight: 500;">
|
||||
No installments found for this entry
|
||||
</div>
|
||||
<div style="font-size: 14px; color: #9ba5bb; margin-top: 8px;">
|
||||
Add your first installment to get started
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
} else {
|
||||
entry.installments.forEach((ins, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${idx === 0 ? 'Original Entry' : 'Installment ' + idx}</td>
|
||||
<td>${escapeHtml(ins.proc_date)}</td>
|
||||
<td>${escapeHtml(ins.description)}</td>
|
||||
<td>${escapeHtml(ins.region)}</td>
|
||||
<td>${formatCurrency(ins.amount)}</td>
|
||||
|
||||
// Determine installment label
|
||||
let installmentLabel = 'Original Entry';
|
||||
if (idx > 0) {
|
||||
installmentLabel = `Installment ${idx}`;
|
||||
}
|
||||
|
||||
// Status dropdown options with colors
|
||||
const statusOptions = [
|
||||
{ value: 'Pending', label: '⏳ Pending', color: '#f59e0b' },
|
||||
{ value: 'Loading', label: '📦 Loading', color: '#3b82f6' },
|
||||
{ value: 'Packed', label: '📦 Packed', color: '#8b5cf6' },
|
||||
{ value: 'Dispatched', label: '🚚 Dispatched', color: '#10b981' },
|
||||
{ value: 'Delivered', label: '✅ Delivered', color: '#0c6b2e' }
|
||||
];
|
||||
|
||||
let statusOptionsHtml = '';
|
||||
statusOptions.forEach(opt => {
|
||||
const selected = ins.status === opt.value ? 'selected' : '';
|
||||
statusOptionsHtml += `<option value="${opt.value}" ${selected} style="color: ${opt.color}; font-weight: 500;">${opt.label}</option>`;
|
||||
});
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div style="width: 32px; height: 32px; border-radius: 8px; background: linear-gradient(135deg, #f0f7ff, #e6f0ff); display: flex; align-items: center; justify-content: center; font-weight: 700; color: var(--primary-1);">
|
||||
${idx + 1}
|
||||
</div>
|
||||
<span style="font-weight: 600;">${installmentLabel}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
||||
<line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/>
|
||||
<line x1="3" y1="10" x2="21" y2="10"/>
|
||||
</svg>
|
||||
${escapeHtml(ins.proc_date || ins.date || '')}
|
||||
</div>
|
||||
</td>
|
||||
<td>${escapeHtml(ins.description || '')}</td>
|
||||
<td>
|
||||
<span style="padding: 4px 8px; background: #f0f7ff; border-radius: 6px; font-size: 12px; font-weight: 600; color: var(--primary-1);">
|
||||
${escapeHtml(ins.region || '')}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span style="font-weight: 700; color: var(--primary-1);">
|
||||
${formatCurrency(ins.amount || 0)}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<select class="installment-status-dropdown"
|
||||
data-id="${ins.id}"
|
||||
onchange="updateInstallmentStatus(${ins.id}, this.value)">
|
||||
<option value="Pending" ${ins.status === 'Pending' ? 'selected' : ''}
|
||||
style="color: #f59e0b; font-weight: 500; padding: 10px;">
|
||||
⏳ Pending
|
||||
</option>
|
||||
<option value="Loading" ${ins.status === 'Loading' ? 'selected' : ''}
|
||||
style="color: #3b82f6; font-weight: 500; padding: 10px;">
|
||||
📦 Loading
|
||||
</option>
|
||||
<option value="Packed" ${ins.status === 'Packed' ? 'selected' : ''}
|
||||
style="color: #8b5cf6; font-weight: 500; padding: 10px;">
|
||||
📦 Packed
|
||||
</option>
|
||||
<option value="Dispatched" ${ins.status === 'Dispatched' ? 'selected' : ''}
|
||||
style="color: #10b981; font-weight: 500; padding: 10px;">
|
||||
🚚 Dispatched
|
||||
</option>
|
||||
<option value="Delivered" ${ins.status === 'Delivered' ? 'selected' : ''}
|
||||
style="color: #0c6b2e; font-weight: 500; padding: 10px;">
|
||||
✅ Delivered
|
||||
</option>
|
||||
onchange="updateInstallmentStatus(${ins.id}, this.value)"
|
||||
title="Update installment status">
|
||||
${statusOptionsHtml}
|
||||
</select>
|
||||
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
// Show modal
|
||||
document.getElementById('entryDetailsModal').classList.add('modal-open');
|
||||
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,548 +1,96 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Chat Support Dashboard')
|
||||
@section('page-title', 'Chat Support')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
:root {
|
||||
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--success-gradient: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
--danger-gradient: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%);
|
||||
--warning-gradient: linear-gradient(135deg, #f7971e 0%, #ffd200 100%);
|
||||
--info-gradient: linear-gradient(135deg, #56ccf2 0%, #2f80ed 100%);
|
||||
--card-shadow: 0 3px 10px rgba(0,0,0,0.05);
|
||||
--card-shadow-hover: 0 6px 16px rgba(0,0,0,0.10);
|
||||
--border-radius: 8px;
|
||||
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
<div class="container py-4">
|
||||
|
||||
.chat-dashboard {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 0.75rem;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
<h2 class="mb-4 fw-bold">Customer Support Chat</h2>
|
||||
|
||||
.dashboard-header {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: clamp(1.4rem, 2.5vw, 2rem);
|
||||
font-weight: 800;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin: 0 0 0.4rem 0;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dashboard-title::before {
|
||||
content: '💬';
|
||||
position: absolute;
|
||||
left: -1.6rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 1.3rem;
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% { transform: translateY(-50%) translateY(0); }
|
||||
40% { transform: translateY(-50%) translateY(-5px); }
|
||||
60% { transform: translateY(-50%) translateY(-2px); }
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
color: #64748b;
|
||||
font-size: 0.8rem;
|
||||
max-width: 380px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* 🔔 GLOBAL NEW MESSAGE COUNTER */
|
||||
.global-notify {
|
||||
margin: 0 auto 0.75rem auto;
|
||||
max-width: 320px;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(15,23,42,0.03);
|
||||
border: 1px dashed #cbd5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.78rem;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.global-notify-badge {
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
min-width: 18px;
|
||||
padding: 0 0.35rem;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 0 0 2px rgba(254, 226, 226, 0.8);
|
||||
}
|
||||
|
||||
.global-notify.d-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tickets-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.75rem 0.9rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
/* max-height / overflow काढले, जेणेकरून बाहेरचा page scroll वापरला जाईल */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tickets-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.6rem;
|
||||
padding-bottom: 0.45rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.tickets-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.tickets-count {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ticket-item {
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.6rem;
|
||||
margin-bottom: 0.3rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: var(--primary-gradient);
|
||||
transform: scaleY(0);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.ticket-item:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--card-shadow-hover);
|
||||
border-color: rgba(102, 126, 234, 0.35);
|
||||
}
|
||||
|
||||
.ticket-item:hover::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.ticket-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.ticket-avatar {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 6px;
|
||||
background: var(--info-gradient);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 3px 8px rgba(86, 204, 242, 0.25);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-avatar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
transition: left 0.4s;
|
||||
}
|
||||
|
||||
.ticket-item:hover .ticket-avatar::after {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.ticket-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ticket-name {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 0 0 0.08rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.unread-count {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 6px rgba(239, 68, 68, 0.4);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.ticket-preview {
|
||||
color: #64748b;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.25;
|
||||
margin-bottom: 0.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
max-height: 1.8em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-time {
|
||||
font-size: 0.65rem;
|
||||
color: #94a3b8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
.ticket-time svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ticket-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 0.3rem;
|
||||
border-top: 1px dashed #e5e7eb;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.14rem 0.45rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.status-open { background: var(--success-gradient); color: white; }
|
||||
.status-closed{ background: var(--danger-gradient); color: white; }
|
||||
|
||||
.chat-btn {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
font-size: 0.7rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.25);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.35);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat-btn::after {
|
||||
content: '→';
|
||||
transition: margin-left 0.25s ease;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.chat-btn:hover::after {
|
||||
margin-left: 0.18rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 1.6rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: var(--border-radius);
|
||||
border: 2px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 2.8rem;
|
||||
margin-bottom: 0.9rem;
|
||||
display: block;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.empty-subtitle {
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.ticket-id {
|
||||
background: rgba(102, 126, 234, 0.08);
|
||||
color: #667eea;
|
||||
padding: 0.12rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
.new-message-dot {
|
||||
position: absolute;
|
||||
top: 0.45rem;
|
||||
right: 0.5rem;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: #ef4444;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.3);
|
||||
animation: blink 1.5s infinite;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(1.1); }
|
||||
}
|
||||
|
||||
/* इथे आता inner scroll नाही */
|
||||
.tickets-list {
|
||||
/* flex: 1; काढला, overflow काढला, parent + body scroll वापरेल */
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar-thumb {
|
||||
background: var(--primary-gradient);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chat-dashboard {
|
||||
padding: 0.6rem;
|
||||
}
|
||||
|
||||
.tickets-container {
|
||||
/* max-height काढलेले, mobile वरही outer scroll */
|
||||
}
|
||||
|
||||
.ticket-header {
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.ticket-footer {
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.chat-btn {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="chat-dashboard">
|
||||
<!-- Header -->
|
||||
<div class="dashboard-header">
|
||||
<h1 class="dashboard-title">Live Chat Dashboard</h1>
|
||||
<p class="dashboard-subtitle">
|
||||
Monitor customer conversations with real-time updates
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 🔔 GLOBAL NEW MESSAGES NOTIFICATION -->
|
||||
<div id="globalNewMessageBox" class="global-notify d-none">
|
||||
<span>New messages:</span>
|
||||
<span id="globalNewMessageCount" class="global-notify-badge">0</span>
|
||||
</div>
|
||||
|
||||
<!-- Tickets Container -->
|
||||
<div class="tickets-container">
|
||||
<div class="tickets-header">
|
||||
<div>
|
||||
<h2 class="tickets-title">
|
||||
📋 Active Conversations
|
||||
<span class="tickets-count">{{ $tickets->count() }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
|
||||
@if($tickets->count() === 0)
|
||||
<!-- Empty State -->
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">💬</div>
|
||||
<h3 class="empty-title">No Active Conversations</h3>
|
||||
<p class="empty-subtitle">
|
||||
Customer conversations will appear here with real-time notifications.
|
||||
</p>
|
||||
<div class="ticket-id">Ready for support requests</div>
|
||||
<div class="p-4 text-center text-muted">
|
||||
<h5>No customer chats yet.</h5>
|
||||
</div>
|
||||
@else
|
||||
<!-- Tickets List -->
|
||||
<div class="tickets-list">
|
||||
<ul class="list-group list-group-flush">
|
||||
|
||||
@foreach($tickets as $ticket)
|
||||
<div class="ticket-item" data-ticket-id="{{ $ticket->id }}">
|
||||
@if($ticket->unread_count > 0)
|
||||
<div class="new-message-dot"></div>
|
||||
@endif
|
||||
|
||||
<div class="ticket-header">
|
||||
<div class="ticket-avatar">
|
||||
{{ strtoupper(substr($ticket->user->customer_name ?? $ticket->user->name, 0, 1)) }}
|
||||
</div>
|
||||
<div class="ticket-content">
|
||||
<div class="ticket-name">
|
||||
{{ $ticket->user->customer_name ?? $ticket->user->name }}
|
||||
@if($ticket->unread_count > 0)
|
||||
<span id="badge-{{ $ticket->id }}" class="unread-count">
|
||||
{{ $ticket->unread_count }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@php
|
||||
// Get last message
|
||||
$lastMsg = $ticket->messages()->latest()->first();
|
||||
@endphp
|
||||
|
||||
<div class="ticket-preview">
|
||||
<li class="list-group-item py-3">
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
|
||||
<!-- Left side: User info + last message -->
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
|
||||
<!-- Profile Circle -->
|
||||
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center"
|
||||
style="width: 45px; height: 45px; font-size: 18px;">
|
||||
{{ strtoupper(substr($ticket->user->customer_name ?? $ticket->user->name, 0, 1)) }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- Customer Name -->
|
||||
<h6 class="mb-1 fw-semibold">
|
||||
{{ $ticket->user->customer_name ?? $ticket->user->name }}
|
||||
</h6>
|
||||
|
||||
<!-- Last message preview -->
|
||||
<small class="text-muted">
|
||||
@if($lastMsg)
|
||||
@if($lastMsg->message)
|
||||
{{ Str::limit($lastMsg->message, 45) }}
|
||||
@elseif(Str::startsWith($lastMsg->file_type, 'image'))
|
||||
📷 Photo shared
|
||||
{{ Str::limit($lastMsg->message, 35) }}
|
||||
@elseif($lastMsg->file_type === 'image')
|
||||
📷 Image
|
||||
@elseif($lastMsg->file_type === 'video')
|
||||
🎥 Video
|
||||
@else
|
||||
📎 File attached
|
||||
📎 Attachment
|
||||
@endif
|
||||
@else
|
||||
<em>Conversation started</em>
|
||||
<i>No messages yet</i>
|
||||
@endif
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@if($lastMsg)
|
||||
<div class="ticket-time">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
|
||||
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
|
||||
</svg>
|
||||
{{ $lastMsg->created_at->diffForHumans() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ticket-footer">
|
||||
<span class="status-badge status-{{ $ticket->status }}">
|
||||
<!-- Right Side: Status + Button -->
|
||||
<div class="text-end">
|
||||
|
||||
<!-- Ticket Status -->
|
||||
<span class="badge
|
||||
{{ $ticket->status === 'open' ? 'bg-success' : 'bg-danger' }}">
|
||||
{{ ucfirst($ticket->status) }}
|
||||
</span>
|
||||
<a href="{{ route('admin.chat.open', $ticket->id) }}" class="chat-btn">
|
||||
Open Chat
|
||||
|
||||
<!-- Open Chat Button -->
|
||||
<a href="{{ route('admin.chat.open', $ticket->id) }}"
|
||||
class="btn btn-sm btn-primary ms-2">
|
||||
Open Chat →
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
</ul>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
710
resources/views/admin/container.blade.php
Normal file
710
resources/views/admin/container.blade.php
Normal file
@@ -0,0 +1,710 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Containers')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #4c6fff;
|
||||
--primary-gradient: linear-gradient(135deg, #4c6fff, #8e54e9);
|
||||
--success-color: #10b981;
|
||||
--warning-color: #f59e0b;
|
||||
--danger-color: #ef4444;
|
||||
--info-color: #3b82f6;
|
||||
--light-bg: #f8fafc;
|
||||
--dark-text: #1e293b;
|
||||
--gray-text: #64748b;
|
||||
--border-color: #e2e8f0;
|
||||
--shadow-sm: 0 1px 3px rgba(0,0,0,0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1);
|
||||
--shadow-lg: 0 10px 25px -5px rgba(0,0,0,0.1);
|
||||
--radius-lg: 16px;
|
||||
--radius-md: 12px;
|
||||
--radius-sm: 8px;
|
||||
}
|
||||
|
||||
.containers-wrapper {
|
||||
min-height: calc(100vh - 180px);
|
||||
padding: 20px 15px;
|
||||
background: linear-gradient(135deg, #f6f9ff 0%, #f0f4ff 100%);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
color: var(--gray-text);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.add-container-btn {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 28px;
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 12px rgba(76, 111, 255, 0.3);
|
||||
}
|
||||
|
||||
.add-container-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(76, 111, 255, 0.4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.add-container-btn i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: 1px solid rgba(255,255,255,0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.filter-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--dark-text);
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-title i {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-text);
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.filter-input, .filter-select, .filter-date {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 14px;
|
||||
color: var(--dark-text);
|
||||
background: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.filter-input:focus, .filter-select:focus, .filter-date:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(76, 111, 255, 0.1);
|
||||
}
|
||||
|
||||
.filter-input::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.apply-btn {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 46px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.apply-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 20px rgba(76, 111, 255, 0.3);
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: white;
|
||||
color: var(--gray-text);
|
||||
border: 2px solid var(--border-color);
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 46px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.main-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-lg);
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid rgba(255,255,255,0.9);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 24px;
|
||||
background: linear-gradient(135deg, #4c6fff, #8e54e9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.card-header h2 i {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stats-badge {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
margin-left: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.container-item {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.container-item:hover {
|
||||
background: #f8fafc;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.container-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.container-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.container-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.container-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-gradient);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4px 12px rgba(76, 111, 255, 0.3);
|
||||
}
|
||||
|
||||
.container-details h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--dark-text);
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.container-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--gray-text);
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.meta-item i {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.3px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.status-badge i {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.status-in-progress {
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
background: #e0f2fe;
|
||||
color: #0369a1;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
background: #0ea5e9;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.update-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
color: var(--dark-text);
|
||||
background: white;
|
||||
min-width: 140px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.update-btn {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.update-btn:hover {
|
||||
background: #3b5de6;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.no-results-icon {
|
||||
font-size: 64px;
|
||||
color: var(--border-color);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.no-results h4 {
|
||||
font-size: 18px;
|
||||
color: var(--gray-text);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.no-results p {
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
box-shadow: var(--shadow-md);
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.success-message i {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 🔥 Totals section */
|
||||
.totals-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
border-radius: var(--radius-md);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.total-card {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.total-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.total-value {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.total-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-text);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
.add-container-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.filter-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.container-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.update-form {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
.status-select, .update-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="containers-wrapper">
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>Container Management</h1>
|
||||
<div class="header-subtitle">
|
||||
Manage all containers, track status, and view entries in real-time
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('containers.create') }}" class="add-container-btn">
|
||||
<i class="fas fa-plus-circle"></i>
|
||||
Add New Container
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="success-message">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>{{ session('success') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="filter-card">
|
||||
<div class="filter-title">
|
||||
<i class="fas fa-filter"></i>
|
||||
Filter Containers
|
||||
</div>
|
||||
<form method="GET" class="filter-grid">
|
||||
<div class="filter-group">
|
||||
<label><i class="fas fa-search"></i> Search</label>
|
||||
<input type="text" name="search" class="filter-input"
|
||||
placeholder="Search by container name or number..."
|
||||
value="{{ request('search') }}">
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label><i class="fas fa-tag"></i> Status</label>
|
||||
<select name="status" class="filter-select">
|
||||
<option value="">All Status</option>
|
||||
<option value="pending" {{ request('status') == 'pending' ? 'selected' : '' }}>Pending</option>
|
||||
<option value="in-progress" {{ request('status') == 'in-progress' ? 'selected' : '' }}>In Progress</option>
|
||||
<option value="completed" {{ request('status') == 'completed' ? 'selected' : '' }}>Completed</option>
|
||||
<option value="cancelled" {{ request('status') == 'cancelled' ? 'selected' : '' }}>Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label><i class="fas fa-calendar"></i> Date</label>
|
||||
<input type="date" name="date" class="filter-date" value="{{ request('date') }}">
|
||||
</div>
|
||||
|
||||
<div class="filter-actions">
|
||||
<button type="submit" class="apply-btn">
|
||||
<i class="fas fa-search"></i> Apply Filters
|
||||
</button>
|
||||
<a href="{{ route('containers.index') }}" class="reset-btn">
|
||||
<i class="fas fa-redo"></i> Reset
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="main-card">
|
||||
<div class="card-header">
|
||||
<h2>
|
||||
<i class="fas fa-boxes"></i>
|
||||
Containers List
|
||||
<span class="stats-badge">{{ $containers->count() }} containers</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@if($containers->isEmpty())
|
||||
<div class="no-results">
|
||||
<div class="no-results-icon">
|
||||
<i class="fas fa-box-open"></i>
|
||||
</div>
|
||||
<h4>No containers found</h4>
|
||||
<p>Get started by creating your first container</p>
|
||||
</div>
|
||||
@else
|
||||
@foreach($containers as $container)
|
||||
@php
|
||||
$status = $container->status;
|
||||
$statusClass = match ($status) {
|
||||
'completed' => 'status-completed',
|
||||
'in-progress' => 'status-in-progress',
|
||||
'cancelled' => 'status-cancelled',
|
||||
default => 'status-pending',
|
||||
};
|
||||
@endphp
|
||||
|
||||
<div class="container-item">
|
||||
<div class="container-header">
|
||||
<div class="container-info">
|
||||
<div class="container-avatar">
|
||||
{{ substr($container->container_name, 0, 2) }}
|
||||
</div>
|
||||
<div class="container-details">
|
||||
<h3>{{ $container->container_name }}</h3>
|
||||
<div class="container-meta">
|
||||
<div class="meta-item">
|
||||
<i class="fas fa-hashtag"></i>
|
||||
<span>{{ $container->container_number }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<i class="fas fa-calendar"></i>
|
||||
<span>{{ $container->container_date?->format('M d, Y') ?: 'No date' }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<i class="fas fa-list"></i>
|
||||
<span>{{ $container->rows->count() }} entries</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<span class="status-badge {{ $statusClass }}">
|
||||
<i class="fas fa-circle"></i>
|
||||
{{ ucfirst(str_replace('-', ' ', $status)) }}
|
||||
</span>
|
||||
|
||||
<a href="{{ route('containers.show', $container->id) }}" class="action-btn view-btn">
|
||||
<i class="fas fa-eye"></i> View
|
||||
</a>
|
||||
|
||||
<form action="{{ route('containers.update-status', $container->id) }}"
|
||||
method="POST" class="update-form">
|
||||
@csrf
|
||||
<select name="status" class="status-select">
|
||||
<option value="pending" {{ $status === 'pending' ? 'selected' : '' }}>Pending</option>
|
||||
<option value="in-progress" {{ $status === 'in-progress' ? 'selected' : '' }}>In Progress</option>
|
||||
<option value="completed" {{ $status === 'completed' ? 'selected' : '' }}>Completed</option>
|
||||
<option value="cancelled" {{ $status === 'cancelled' ? 'selected' : '' }}>Cancelled</option>
|
||||
</select>
|
||||
<button type="submit" class="update-btn">
|
||||
<i class="fas fa-sync-alt"></i> Update
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form action="{{ route('containers.destroy', $container->id) }}" method="POST"
|
||||
onsubmit="return confirm('Are you sure you want to delete this container and all its entries?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="action-btn delete-btn">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 🔥 Totals instead of first row preview -->
|
||||
<div class="totals-section">
|
||||
<div class="total-card">
|
||||
<div class="total-value">{{ number_format($container->summary['total_ctn'], 1) }}</div>
|
||||
<div class="total-label">Total CTN</div>
|
||||
</div>
|
||||
<div class="total-card">
|
||||
<div class="total-value">{{ number_format($container->summary['total_qty'], 0) }}</div>
|
||||
<div class="total-label">Total QTY</div>
|
||||
</div>
|
||||
<div class="total-card">
|
||||
<div class="total-value">{{ number_format($container->summary['total_cbm'], 3) }}</div>
|
||||
<div class="total-label">Total CBM</div>
|
||||
</div>
|
||||
<div class="total-card">
|
||||
<div class="total-value">{{ number_format($container->summary['total_kg'], 1) }}</div>
|
||||
<div class="total-label">Total KG</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
|
||||
@endsection
|
||||
251
resources/views/admin/container_create.blade.php
Normal file
251
resources/views/admin/container_create.blade.php
Normal file
@@ -0,0 +1,251 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Add Container')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
.cm-add-wrapper {
|
||||
padding: 10px 0 20px 0;
|
||||
}
|
||||
|
||||
.cm-add-header-card {
|
||||
border-radius: 14px;
|
||||
border: none;
|
||||
margin-bottom: 18px;
|
||||
background: linear-gradient(90deg,#4c6fff,#8e54e9);
|
||||
box-shadow: 0 6px 18px rgba(15,35,52,0.18);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.cm-add-header-card .card-body {
|
||||
padding: 14px 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cm-add-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cm-add-sub {
|
||||
font-size: 12px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.cm-add-main-card {
|
||||
border-radius: 14px;
|
||||
border: none;
|
||||
box-shadow: 0 6px 18px rgba(15,35,52,0.12);
|
||||
}
|
||||
|
||||
.cm-add-main-card .card-header {
|
||||
background:#ffffff;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
padding: 10px 18px;
|
||||
}
|
||||
|
||||
.cm-add-main-card .card-header h5 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cm-form-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color:#495057;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.cm-form-control {
|
||||
font-size: 13px;
|
||||
border-radius: 10px;
|
||||
border:1px solid #d0d7e2;
|
||||
padding: 8px 11px;
|
||||
}
|
||||
|
||||
.cm-form-control:focus {
|
||||
border-color:#4c6fff;
|
||||
box-shadow:0 0 0 0.15rem rgba(76,111,255,.25);
|
||||
}
|
||||
|
||||
.cm-help-text {
|
||||
font-size: 11px;
|
||||
color:#868e96;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.cm-btn-primary {
|
||||
border-radius: 20px;
|
||||
padding: 6px 22px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cm-btn-secondary {
|
||||
border-radius: 20px;
|
||||
padding: 6px 18px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cm-error-list {
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container-fluid cm-add-wrapper">
|
||||
|
||||
{{-- TOP GRADIENT HEADER --}}
|
||||
<div class="card cm-add-header-card">
|
||||
<div class="card-body">
|
||||
<div>
|
||||
<h4 class="cm-add-title">Create New Container</h4>
|
||||
<div class="cm-add-sub">
|
||||
Add container details and upload Kent loading list Excel file.
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('containers.index') }}" class="btn btn-light btn-sm">
|
||||
Back to Containers
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- MAIN CARD --}}
|
||||
<div class="card cm-add-main-card">
|
||||
<div class="card-header">
|
||||
<h5>Add Container</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
{{-- SUCCESS MESSAGE --}}
|
||||
@if (session('success'))
|
||||
<div class="alert alert-success">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- VALIDATION ERRORS --}}
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<ul class="mb-0 cm-error-list">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- UNMATCHED ROWS TABLE --}}
|
||||
@if (session('unmatched_rows'))
|
||||
<div class="alert alert-warning mt-3">
|
||||
<strong>Mark number not matched:</strong>
|
||||
|
||||
|
||||
@php
|
||||
$unmatchedRows = session('unmatched_rows');
|
||||
$headings = [];
|
||||
if (!empty($unmatchedRows)) {
|
||||
$headings = array_keys($unmatchedRows[0]['data'] ?? []);
|
||||
// इथे Excel मधला 'MARK' कॉलम hide करतो, कारण आधीच Mark No वेगळा column आहे
|
||||
$headings = array_filter($headings, function ($h) {
|
||||
return strtoupper(trim($h)) !== 'MARK';
|
||||
});
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if(!empty($unmatchedRows))
|
||||
<div class="table-responsive" style="max-height:260px; overflow:auto; border:1px solid #e3e6ef;">
|
||||
<table class="table table-sm table-bordered mb-0" style="font-size:11.5px; min-width:800px;">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Excel Row</th>
|
||||
<th>Mark No</th>
|
||||
@foreach($headings as $head)
|
||||
<th>{{ $head }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($unmatchedRows as $row)
|
||||
<tr>
|
||||
<td>{{ $row['excel_row'] }}</td>
|
||||
<td>{{ $row['mark_no'] }}</td>
|
||||
@foreach($headings as $head)
|
||||
<td>{{ $row['data'][$head] ?? '' }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- FORM: unmatched_rows असल्यावर form लपवायचा असेल तर खालील condition ठेवा --}}
|
||||
@if (!session('unmatched_rows'))
|
||||
<form action="{{ route('containers.store') }}" method="POST" enctype="multipart/form-data" class="mt-3">
|
||||
@csrf
|
||||
|
||||
<div class="row g-3">
|
||||
{{-- Container Name --}}
|
||||
<div class="col-md-6">
|
||||
<label class="cm-form-label">Container Name</label>
|
||||
<input type="text"
|
||||
name="container_name"
|
||||
class="form-control cm-form-control"
|
||||
value="{{ old('container_name') }}"
|
||||
placeholder="Enter container name">
|
||||
</div>
|
||||
|
||||
{{-- Container Number --}}
|
||||
<div class="col-md-6">
|
||||
<label class="cm-form-label">Container Number</label>
|
||||
<input type="text"
|
||||
name="container_number"
|
||||
class="form-control cm-form-control"
|
||||
value="{{ old('container_number') }}"
|
||||
placeholder="Enter container number">
|
||||
</div>
|
||||
|
||||
{{-- Container Date --}}
|
||||
<div class="col-md-6">
|
||||
<label class="cm-form-label">Container Date</label>
|
||||
<input type="date"
|
||||
name="container_date"
|
||||
class="form-control cm-form-control"
|
||||
value="{{ old('container_date') }}">
|
||||
</div>
|
||||
|
||||
{{-- Excel File --}}
|
||||
<div class="col-md-6">
|
||||
<label class="cm-form-label">Loading List Excel</label>
|
||||
<input type="file"
|
||||
name="excel_file"
|
||||
class="form-control cm-form-control"
|
||||
accept=".xls,.xlsx">
|
||||
<div class="cm-help-text">
|
||||
Upload Kent loading list Excel file (.xls / .xlsx).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary cm-btn-primary">
|
||||
Save Container
|
||||
</button>
|
||||
<a href="{{ route('containers.index') }}" class="btn btn-outline-secondary cm-btn-secondary">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
291
resources/views/admin/container_show.blade.php
Normal file
291
resources/views/admin/container_show.blade.php
Normal file
@@ -0,0 +1,291 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Container Details')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
.cm-detail-wrapper {
|
||||
padding: 10px 0 20px 0;
|
||||
}
|
||||
.cm-detail-header-card {
|
||||
border-radius: 14px;
|
||||
border: none;
|
||||
margin-bottom: 18px;
|
||||
background: linear-gradient(90deg,#4c6fff,#8e54e9);
|
||||
box-shadow: 0 6px 18px rgba(15,35,52,0.18);
|
||||
color:#ffffff;
|
||||
}
|
||||
.cm-detail-header-card .card-body {
|
||||
padding: 14px 18px;
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
}
|
||||
.cm-detail-title {
|
||||
margin:0;
|
||||
font-size:20px;
|
||||
font-weight:600;
|
||||
}
|
||||
.cm-detail-sub {
|
||||
font-size:12px;
|
||||
opacity:0.9;
|
||||
}
|
||||
.cm-detail-main-card {
|
||||
border-radius:14px;
|
||||
border:none;
|
||||
box-shadow:0 6px 18px rgba(15,35,52,0.12);
|
||||
overflow:hidden;
|
||||
}
|
||||
.cm-detail-main-card .card-header {
|
||||
background:#ffffff;
|
||||
border-bottom:1px solid #edf0f5;
|
||||
padding:10px 18px;
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
}
|
||||
.cm-detail-main-card .card-header h5 {
|
||||
margin:0;
|
||||
font-size:16px;
|
||||
font-weight:600;
|
||||
}
|
||||
.cm-info-label {
|
||||
font-size:12px;
|
||||
color:#6c757d;
|
||||
font-weight:500;
|
||||
}
|
||||
.cm-info-value {
|
||||
font-size:13px;
|
||||
font-weight:500;
|
||||
color:#343a40;
|
||||
}
|
||||
.cm-table-wrapper {
|
||||
position:relative;
|
||||
max-height: 520px;
|
||||
overflow:auto;
|
||||
border-top:1px solid #edf0f5;
|
||||
}
|
||||
.cm-table {
|
||||
font-size:11.5px;
|
||||
min-width: 1100px;
|
||||
}
|
||||
.cm-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background: #fff7e0;
|
||||
color:#495057;
|
||||
font-weight:600;
|
||||
border-bottom:1px solid #e0d2a4;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.cm-table tbody tr:nth-child(even) {
|
||||
background:#fafbff;
|
||||
}
|
||||
.cm-table tbody tr:hover {
|
||||
background:#e9f3ff;
|
||||
}
|
||||
.cm-table td,
|
||||
.cm-table th {
|
||||
padding:4px 6px;
|
||||
vertical-align:middle;
|
||||
}
|
||||
.cm-table td {
|
||||
white-space:nowrap;
|
||||
}
|
||||
.cm-table-caption {
|
||||
font-size:11px;
|
||||
color:#868e96;
|
||||
padding:6px 18px 0 18px;
|
||||
}
|
||||
.cm-filter-bar {
|
||||
padding:8px 18px 0 18px;
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
.cm-filter-input {
|
||||
max-width:240px;
|
||||
font-size:12px;
|
||||
border-radius:20px;
|
||||
padding:6px 10px;
|
||||
}
|
||||
.cm-edit-save-btn {
|
||||
font-size:12px;
|
||||
border-radius:20px;
|
||||
padding:6px 14px;
|
||||
}
|
||||
.cm-cell-input {
|
||||
width: 140px;
|
||||
min-width: 120px;
|
||||
max-width: 220px;
|
||||
font-size:11px;
|
||||
padding:3px 4px;
|
||||
height: 26px;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.cm-detail-header-card .card-body {
|
||||
flex-direction:column;
|
||||
align-items:flex-start;
|
||||
}
|
||||
.cm-table-wrapper {
|
||||
max-height:400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container-fluid cm-detail-wrapper">
|
||||
|
||||
{{-- TOP GRADIENT HEADER --}}
|
||||
<div class="card cm-detail-header-card">
|
||||
<div class="card-body">
|
||||
<div>
|
||||
<h4 class="cm-detail-title">
|
||||
Container: {{ $container->container_number }}
|
||||
</h4>
|
||||
<div class="cm-detail-sub">
|
||||
Edit loading list directly – scroll horizontally and vertically like Excel.
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('containers.index') }}" class="btn btn-light btn-sm">
|
||||
← Back to list
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- MAIN CARD --}}
|
||||
<div class="card cm-detail-main-card">
|
||||
<div class="card-header">
|
||||
<h5>Container Information</h5>
|
||||
|
||||
@if(!$container->rows->isEmpty())
|
||||
{{-- Save button (submits form below) --}}
|
||||
<button type="submit"
|
||||
form="cm-edit-rows-form"
|
||||
class="btn btn-primary cm-edit-save-btn">
|
||||
💾 Save Changes
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="card-body pb-0">
|
||||
{{-- BASIC INFO --}}
|
||||
<div class="row g-3 mb-2">
|
||||
<div class="col-md-4">
|
||||
<div class="cm-info-label">Container</div>
|
||||
<div class="cm-info-value">{{ $container->container_name }}</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="cm-info-label">Date</div>
|
||||
<div class="cm-info-value">
|
||||
{{ $container->container_date?->format('d-m-Y') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="cm-info-label">Excel File</div>
|
||||
@if($container->excel_file)
|
||||
<div class="cm-info-value">
|
||||
<a href="{{ \Illuminate\Support\Facades\Storage::url($container->excel_file) }}"
|
||||
target="_blank">
|
||||
Download / View Excel
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="cm-info-value text-muted">Not uploaded</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($container->rows->isEmpty())
|
||||
<div class="p-3">
|
||||
<p class="mb-0">No entries found for this container.</p>
|
||||
</div>
|
||||
@else
|
||||
@php
|
||||
// सर्व headings collect
|
||||
$allHeadings = [];
|
||||
foreach ($container->rows as $row) {
|
||||
if (is_array($row->data)) {
|
||||
$allHeadings = array_unique(array_merge($allHeadings, array_keys($row->data)));
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
{{-- FILTER BAR --}}
|
||||
<div class="cm-filter-bar">
|
||||
<div class="cm-table-caption">
|
||||
Total rows: {{ $container->rows->count() }} • Type to filter rows, edit cells then click "Save Changes".
|
||||
</div>
|
||||
<input type="text" id="cmRowSearch" class="form-control cm-filter-input"
|
||||
placeholder="Quick search in table..." onkeyup="cmFilterRows()">
|
||||
</div>
|
||||
|
||||
{{-- EDITABLE TABLE FORM --}}
|
||||
<form id="cm-edit-rows-form"
|
||||
action="{{ route('containers.rows.update', $container->id) }}"
|
||||
method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="cm-table-wrapper mt-1">
|
||||
<table class="table table-bordered table-hover cm-table" id="cmExcelTable">
|
||||
<thead>
|
||||
<tr>
|
||||
@foreach($allHeadings as $heading)
|
||||
<th>{{ $heading }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($container->rows as $row)
|
||||
<tr>
|
||||
@foreach($allHeadings as $heading)
|
||||
@php
|
||||
$value = $row->data[$heading] ?? '';
|
||||
@endphp
|
||||
<td>
|
||||
<input type="text"
|
||||
class="form-control form-control-sm cm-cell-input"
|
||||
name="rows[{{ $row->id }}][{{ $heading }}]"
|
||||
value="{{ $value }}">
|
||||
</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- SIMPLE FRONT‑END SEARCH --}}
|
||||
<script>
|
||||
function cmFilterRows() {
|
||||
const input = document.getElementById('cmRowSearch');
|
||||
if (!input) return;
|
||||
const filter = input.value.toLowerCase();
|
||||
const table = document.getElementById('cmExcelTable');
|
||||
if (!table) return;
|
||||
|
||||
const rows = table.getElementsByTagName('tr');
|
||||
for (let i = 1; i < rows.length; i++) { // skip header
|
||||
const cells = rows[i].getElementsByTagName('td');
|
||||
let match = false;
|
||||
for (let j = 0; j < cells.length; j++) {
|
||||
const txt = cells[j].textContent || cells[j].innerText;
|
||||
if (txt.toLowerCase().indexOf(filter) > -1) {
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
rows[i].style.display = match ? '' : 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
@@ -1,10 +1,10 @@
|
||||
@extends('admin.layouts.app')
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Customers')
|
||||
@section('page-title', 'Customers')
|
||||
|
||||
@section('content')
|
||||
@section('content')
|
||||
|
||||
<style>
|
||||
<style>
|
||||
/* Import Inter font */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
@@ -700,9 +700,9 @@
|
||||
padding: 2px 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="container-fluid">
|
||||
<!-- Header - Removed gradient -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 style="color: #2c3e50; font-weight: 700; font-family: 'Inter', sans-serif;">Customer List</h4>
|
||||
@@ -830,6 +830,7 @@
|
||||
<th class="table-header">Order Total</th>
|
||||
<th class="table-header">Total Payable</th> {{-- NEW --}}
|
||||
<th class="table-header">Remaining</th> {{-- NEW --}}
|
||||
<th class="table-header">Paid Amount</th>
|
||||
<th class="table-header">Create Date</th>
|
||||
<th class="table-header">Status</th>
|
||||
<th class="table-header" width="100">Actions</th>
|
||||
@@ -907,8 +908,11 @@
|
||||
@endif
|
||||
</td>
|
||||
|
||||
|
||||
|
||||
<td class="total-column">
|
||||
<span class="fw-bold text-success">
|
||||
₹{{ number_format($totalPaid, 2) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
|
||||
<!-- Create Date -->
|
||||
@@ -984,9 +988,9 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add hover effects to table rows
|
||||
const tableRows = document.querySelectorAll('.table tbody tr');
|
||||
@@ -1013,6 +1017,6 @@
|
||||
@endif
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
@endsection
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 17px 17px 0 0;
|
||||
background: #fceeb8ff;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 54px;
|
||||
padding: 15px 26px 10px 22px;
|
||||
border-bottom: 1.4px solid #e8e2cf;
|
||||
@@ -37,7 +37,7 @@
|
||||
.invoice-management-title {
|
||||
font-size: 1.32rem;
|
||||
font-weight: 800;
|
||||
color: #2451af;
|
||||
color: #ffffffff;
|
||||
letter-spacing: .08em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -223,7 +223,7 @@
|
||||
|
||||
/* Center all table content */
|
||||
.table thead tr {
|
||||
background: #feebbe !important;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.table thead th:first-child {
|
||||
@@ -237,7 +237,7 @@
|
||||
background: transparent !important;
|
||||
border: none;
|
||||
font-weight: 700;
|
||||
color: #343535;
|
||||
color: #ffffffff;
|
||||
letter-spacing: 0.02em;
|
||||
font-size: 14px;
|
||||
padding: 20px 15px;
|
||||
@@ -259,24 +259,32 @@
|
||||
/* Soft blue background for ALL table rows */
|
||||
.table-striped tbody tr {
|
||||
background: #f0f8ff !important;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
transition: all 0.15s ease;
|
||||
|
||||
.table-striped tbody tr:hover {
|
||||
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
|
||||
.table-striped tbody tr td {
|
||||
padding: 6px 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.table-striped tbody tr:hover {
|
||||
background: #e6f3ff !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
||||
transform: translateY(-0.5px);
|
||||
}
|
||||
|
||||
|
||||
/* Remove striped pattern - all rows same soft blue */
|
||||
.table-striped tbody tr:nth-of-type(odd),
|
||||
.table-striped tbody tr:nth-of-type(even) {
|
||||
background: #f0f8ff !important;
|
||||
}
|
||||
|
||||
/* Center all table cells with proper spacing */
|
||||
|
||||
.table td {
|
||||
padding: 18px 15px;
|
||||
border: none;
|
||||
@@ -291,7 +299,7 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* First and last cell rounded corners */
|
||||
|
||||
.table td:first-child {
|
||||
padding-left: 30px;
|
||||
font-weight: 600;
|
||||
@@ -587,7 +595,7 @@
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
color: #64748b;
|
||||
color: #000000ff;
|
||||
font-weight: 500;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,11 +5,17 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>Admin Panel</title>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<!-- ✅ CRITICAL: CSRF Token for Echo -->
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>@yield('page-title', 'Admin Panel')</title>
|
||||
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
|
||||
<style>
|
||||
body {
|
||||
@@ -22,7 +28,6 @@
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* ✨ Sidebar Glass + Animated Highlight Effect */
|
||||
.sidebar {
|
||||
width: 200px;
|
||||
height: 100vh;
|
||||
@@ -41,7 +46,6 @@
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Sidebar collapsed state */
|
||||
.sidebar.collapsed {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
@@ -64,10 +68,11 @@
|
||||
}
|
||||
|
||||
.sidebar .word {
|
||||
color: #800000; font-size: 13px; line-height: 1.24;
|
||||
color: #800000;
|
||||
font-size: 13px;
|
||||
line-height: 1.24;
|
||||
}
|
||||
|
||||
/* 🔥 Sidebar Links */
|
||||
.sidebar a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -84,7 +89,6 @@
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Background Animation */
|
||||
.sidebar a::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -109,7 +113,6 @@
|
||||
color: #1258e0 !important;
|
||||
}
|
||||
|
||||
/* Icon bounce effect */
|
||||
.sidebar a i {
|
||||
margin-right: 8px;
|
||||
font-size: 1.1rem;
|
||||
@@ -121,7 +124,6 @@
|
||||
color: #1258e0 !important;
|
||||
}
|
||||
|
||||
/* Active link glow effect */
|
||||
.sidebar a.active {
|
||||
background: linear-gradient(90deg, rgba(80,120,255,0.15), rgba(120,180,255,0.2));
|
||||
color: #1258e0 !important;
|
||||
@@ -136,7 +138,6 @@
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
/* Logout Button */
|
||||
.sidebar form button {
|
||||
border-radius: 12px;
|
||||
margin-top: 12px;
|
||||
@@ -150,7 +151,6 @@
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
/* 🧭 Main Content */
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
min-height: 100vh;
|
||||
@@ -162,13 +162,11 @@
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Main content when sidebar is collapsed */
|
||||
.main-content.expanded {
|
||||
margin-left: 0;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
/* Header hamburger button */
|
||||
.header-toggle {
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -254,7 +252,10 @@ header .bi-bell .badge {
|
||||
<div class="sidebar">
|
||||
<div class="logo">
|
||||
<img src="{{ asset('images/kent_logo2.png') }}" alt="Kent Logo">
|
||||
<div class="word"><strong>KENT</strong><br /><small>International Pvt. Ltd.</small></div>
|
||||
<div class="word">
|
||||
<strong>KENT</strong><br />
|
||||
<small>International Pvt. Ltd.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Dashboard (requires order.view) --}}
|
||||
@@ -263,12 +264,19 @@ header .bi-bell .badge {
|
||||
<i class="bi bi-house"></i> Dashboard
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
<!--
|
||||
{{-- Shipments --}}
|
||||
@can('shipment.view')
|
||||
<a href="{{ route('admin.shipments') }}" class="{{ request()->routeIs('admin.shipments') ? 'active' : '' }}">
|
||||
<i class="bi bi-truck"></i> Shipments
|
||||
</a>
|
||||
@endcan -->
|
||||
|
||||
{{-- Container – NEW MENU --}}
|
||||
@can('container.view')
|
||||
<a href="{{ route('containers.index') }}" class="{{ request()->routeIs('containers.*') ? 'active' : '' }}">
|
||||
<i class="fa-solid fa-box"></i> Container
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Invoice --}}
|
||||
@@ -292,10 +300,12 @@ header .bi-bell .badge {
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Chat Support (NO PERMISSION REQUIRED) --}}
|
||||
{{-- Chat Support --}}
|
||||
@can('chat_support.view')
|
||||
<a href="{{ route('admin.chat_support') }}" class="{{ request()->routeIs('admin.chat_support') ? 'active' : '' }}">
|
||||
<i class="bi bi-chat-dots"></i> Chat Support
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Orders --}}
|
||||
@can('orders.view')
|
||||
@@ -318,10 +328,12 @@ header .bi-bell .badge {
|
||||
</a>
|
||||
@endcan -->
|
||||
|
||||
{{-- Staff (NO PERMISSION REQUIRED) --}}
|
||||
{{-- Staff --}}
|
||||
@can('staff.view')
|
||||
<a href="{{ route('admin.staff.index') }}" class="{{ request()->routeIs('admin.staff.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-person-badge"></i> Staff
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Account Section --}}
|
||||
@can('account.view')
|
||||
@@ -354,21 +366,30 @@ header .bi-bell .badge {
|
||||
<div class="dropdown">
|
||||
<a href="#" class="d-flex align-items-center text-decoration-none dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<img src="https://i.pravatar.cc/40" class="rounded-circle me-2" width="35" height="35">
|
||||
<span class="dropdown-user-profile-name">{{ Auth::guard('admin')->user()->name ?? 'User' }}</span>
|
||||
<span class="dropdown-user-profile-name">
|
||||
{{ auth('admin')->user()->name ?? 'User' }}
|
||||
</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="{{ route('admin.profile') }}"><i class="bi bi-person-circle me-2"></i>Profile</a></li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ route('admin.profile') }}">
|
||||
<i class="bi bi-person-circle me-2"></i>Profile
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form method="POST" action="{{ route('admin.logout') }}">
|
||||
@csrf
|
||||
<button class="dropdown-item" type="submit"><i class="bi bi-box-arrow-right me-2"></i>Logout</button>
|
||||
<button class="dropdown-item" type="submit">
|
||||
<i class="bi bi-box-arrow-right me-2"></i>Logout
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
@yield('content')
|
||||
</div>
|
||||
@@ -383,13 +404,11 @@ header .bi-bell .badge {
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
|
||||
// Function to toggle sidebar
|
||||
function toggleSidebar() {
|
||||
sidebar.classList.toggle('collapsed');
|
||||
mainContent.classList.toggle('expanded');
|
||||
}
|
||||
|
||||
// Header toggle button click event
|
||||
if (headerToggle) {
|
||||
headerToggle.addEventListener('click', toggleSidebar);
|
||||
}
|
||||
|
||||
@@ -11,59 +11,67 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
.container {
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 850px;
|
||||
margin: 24px auto 0 auto;
|
||||
background: #fff;
|
||||
border-radius: 13px;
|
||||
box-shadow: 0 2px 14px rgba(40,105,160,0.08);
|
||||
padding: 35px 32px 18px 32px;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* ================= HEADER FIX ================= */
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
overflow: hidden; /* clears floats */
|
||||
border-bottom: 2px solid #E6EBF0;
|
||||
padding-bottom: 13px;
|
||||
}
|
||||
.logo-company {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.logo {
|
||||
}
|
||||
|
||||
/* LEFT SIDE */
|
||||
.logo-company {
|
||||
float: left;
|
||||
width: 65%;
|
||||
}
|
||||
|
||||
/* RIGHT SIDE */
|
||||
.invoice-details {
|
||||
float: right;
|
||||
width: 35%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Logo */
|
||||
.logo {
|
||||
height: 50px;
|
||||
margin-right: 13px;
|
||||
}
|
||||
.company-details {
|
||||
margin-top: 0;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* Company text */
|
||||
.company-details {
|
||||
font-size: 15px;
|
||||
}
|
||||
.company-title {
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.company-title {
|
||||
font-size: 21px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.company-sub {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.invoice-details {
|
||||
text-align: right;
|
||||
min-width: 220px;
|
||||
}
|
||||
.invoice-title {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* Invoice */
|
||||
.invoice-title {
|
||||
font-weight: bold;
|
||||
font-size: 23px;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.paid-label {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.paid-tag {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* Paid / Status */
|
||||
.paid-tag {
|
||||
background: #23BF47;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
@@ -72,8 +80,9 @@
|
||||
font-size: 17px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
.paid-tag:before {
|
||||
}
|
||||
|
||||
.paid-tag:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
@@ -82,99 +91,80 @@
|
||||
height: 10px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.paid-date {
|
||||
}
|
||||
|
||||
.paid-date {
|
||||
font-size: 14px;
|
||||
color: #23BF47;
|
||||
margin-top: 2px;
|
||||
}
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.bill-section {
|
||||
/* ================= REST ================= */
|
||||
|
||||
.bill-section {
|
||||
background: #F3F7FB;
|
||||
border-radius: 11px;
|
||||
padding: 20px 18px 13px 18px;
|
||||
margin: 28px 0 16px 0;
|
||||
box-shadow: 0 0px 0px #0000;
|
||||
}
|
||||
.bill-title {
|
||||
}
|
||||
|
||||
.bill-title {
|
||||
font-size: 17px;
|
||||
font-weight: bold;
|
||||
color: #23355D;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.bill-details {
|
||||
}
|
||||
|
||||
.bill-details {
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 9px;
|
||||
margin-bottom: 13px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
th {
|
||||
}
|
||||
|
||||
th {
|
||||
background: #F6F7F9;
|
||||
padding: 10px 0;
|
||||
font-size: 15px;
|
||||
color: #6781A6;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
text-align: left;
|
||||
}
|
||||
td {
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 7px 0;
|
||||
color: #222;
|
||||
font-size: 15px;
|
||||
border: none;
|
||||
text-align: left;
|
||||
}
|
||||
tbody tr:not(:last-child) td {
|
||||
color: #222;
|
||||
}
|
||||
|
||||
tbody tr:not(:last-child) td {
|
||||
border-bottom: 1px solid #E6EBF0;
|
||||
}
|
||||
tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
.totals-row td {
|
||||
font-weight: bold;
|
||||
color: #23355D;
|
||||
}
|
||||
.gst-row td {
|
||||
font-weight: 500;
|
||||
color: #23BF47;
|
||||
}
|
||||
.total-row td {
|
||||
font-weight: bold;
|
||||
font-size: 17px;
|
||||
color: #222;
|
||||
}
|
||||
.payment-info {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 9px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.ref-number {
|
||||
font-size: 14px;
|
||||
color: #6781A6;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.footer {
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: 1.2px solid #E6EBF0;
|
||||
margin-top: 25px;
|
||||
padding-top: 12px;
|
||||
font-size: 16px;
|
||||
color: #888;
|
||||
text-align: center;
|
||||
}
|
||||
.footer strong {
|
||||
color: #222;
|
||||
}
|
||||
.gst-row td {
|
||||
color: #23BF47;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.total-row td {
|
||||
font-weight: bold;
|
||||
font-size: 17px;
|
||||
border-top: 2px solid #E6EBF0;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -241,18 +231,47 @@
|
||||
<td>{{ number_format($item->ttl_amount, 0) }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
{{-- SUBTOTAL --}}
|
||||
<tr class="totals-row">
|
||||
<td colspan="3" style="text-align:right;">Subtotal:</td>
|
||||
<td>{{ number_format($invoice->subtotal, 0) }}</td>
|
||||
<td colspan="3" style="text-align:right;">total</td>
|
||||
<td style="text-align:right;">
|
||||
₹ {{ number_format($invoice->final_amount, 2) }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{{-- TAX --}}
|
||||
@if($invoice->tax_type === 'gst' && $invoice->gst_amount > 0)
|
||||
|
||||
<tr class="gst-row">
|
||||
<td colspan="3" style="text-align:right;">GST ({{ $invoice->gst_percent }}%):</td>
|
||||
<td>{{ number_format($invoice->gst_amount, 0) }}</td>
|
||||
<td colspan="3" style="text-align:right;">
|
||||
GST ({{ $invoice->gst_percent }}%)
|
||||
</td>
|
||||
<td style="text-align:right;">
|
||||
₹ {{ number_format($invoice->gst_amount, 2) }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@elseif($invoice->tax_type === 'igst' && $invoice->gst_amount > 0)
|
||||
|
||||
<tr class="gst-row">
|
||||
<td colspan="3" style="text-align:right;">
|
||||
IGST ({{ $invoice->gst_percent }}%)
|
||||
</td>
|
||||
<td style="text-align:right;">
|
||||
₹ {{ number_format($invoice->gst_amount, 2) }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@endif
|
||||
|
||||
{{-- TOTAL --}}
|
||||
<tr class="total-row">
|
||||
<td colspan="3" style="text-align:right;">Total:</td>
|
||||
<td>{{ number_format($invoice->final_amount_with_gst, 0) }}</td>
|
||||
<td colspan="3" style="text-align:right;">Total Amount</td>
|
||||
<td style="text-align:right;">
|
||||
₹ {{ number_format($invoice->final_amount_with_gst, 2) }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Payment Info & Reference -->
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Professional Invoice</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<link rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
|
||||
<style>
|
||||
/* ALL YOUR EXISTING CSS STAYS HERE - EXCEPT GST TOTALS SECTION REMOVED */
|
||||
:root {
|
||||
--primary: #2c3e50;
|
||||
--secondary: #3498db;
|
||||
@@ -21,7 +24,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f5f7fa;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
@@ -30,14 +33,14 @@
|
||||
.invoice-container {
|
||||
max-width: 1200px;
|
||||
margin: 2rem auto;
|
||||
background: white;
|
||||
background: #ffffff;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.invoice-header {
|
||||
background: white;
|
||||
background: #ffffff;
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
@@ -60,7 +63,7 @@
|
||||
}
|
||||
|
||||
.id-box {
|
||||
background: white;
|
||||
background: #ffffff;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1rem;
|
||||
border: 1px solid #e9ecef;
|
||||
@@ -84,7 +87,6 @@
|
||||
.id-box-secondary {
|
||||
border-left: 4px solid var(--success);
|
||||
}
|
||||
|
||||
.id-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
@@ -92,8 +94,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.id-icon-primary {
|
||||
@@ -105,11 +107,6 @@
|
||||
background: linear-gradient(135deg, #27ae60 0%, #219653 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.id-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.id-label {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
@@ -128,73 +125,64 @@
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Enhanced Date Section with Blue-Purple Gradient */
|
||||
.date-badge {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
background: linear-gradient(135deg, #3498db 0%, #9b59b6 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
min-width: 140px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.2);
|
||||
.date-container {
|
||||
background: #ffffff;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #e9ecef;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
.date-badge:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.3);
|
||||
.date-card {
|
||||
text-align: center;
|
||||
padding: 0.75rem;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.date-badge .badge-label {
|
||||
font-size: 0.7rem;
|
||||
.date-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 0.5rem;
|
||||
background: var(--secondary);
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.date-label {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.date-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
padding: 0.5rem;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.date-connector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.date-badge .badge-label i {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.date-badge .badge-value {
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.date-badge.due-date {
|
||||
background: linear-gradient(135deg, #3498db 0%, #9b59b6 100%);
|
||||
}
|
||||
|
||||
.date-badge.overdue {
|
||||
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.4); }
|
||||
70% { box-shadow: 0 0 0 6px rgba(231, 76, 60, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(231, 76, 60, 0); }
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
color: #dee2e6;
|
||||
font-weight: 300;
|
||||
padding: 0 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.date-separator i {
|
||||
background: white;
|
||||
padding: 8px;
|
||||
.date-connector i {
|
||||
background: var(--light);
|
||||
padding: 10px;
|
||||
border-radius: 50%;
|
||||
color: var(--secondary);
|
||||
border: 2px solid #e9ecef;
|
||||
@@ -266,7 +254,6 @@
|
||||
padding: 0.35rem 0.65rem;
|
||||
}
|
||||
|
||||
/* COMPACT HEADER STYLES */
|
||||
.compact-header {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
@@ -275,13 +262,8 @@
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Date and status row */
|
||||
.date-status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
.compact-header .status-badge {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -289,6 +271,10 @@
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.date-connector {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
@@ -346,18 +332,27 @@
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.invoice-container {
|
||||
box-shadow: none;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="invoice-container">
|
||||
<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">
|
||||
@@ -366,100 +361,91 @@
|
||||
</h2>
|
||||
<h4 class="fw-bold text-dark mb-0">{{ $invoice->invoice_number }}</h4>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 text-end">
|
||||
<div class="date-status-row">
|
||||
<!-- Invoice Date -->
|
||||
<div class="date-badge">
|
||||
<div class="badge-label">
|
||||
<i class="fas fa-calendar-alt"></i> INVOICE DATE
|
||||
</div>
|
||||
<div class="badge-value">{{ \Carbon\Carbon::parse($invoice->invoice_date)->format('M d, Y') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Due Date -->
|
||||
<div class="date-badge due-date @if($invoice->status == 'overdue') overdue @endif">
|
||||
<div class="badge-label">
|
||||
<i class="fas fa-clock"></i> DUE DATE
|
||||
</div>
|
||||
<div class="badge-value">{{ \Carbon\Carbon::parse($invoice->due_date)->format('M d, Y') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Badge -->
|
||||
<span class="status-badge
|
||||
@if($invoice->status=='paid') bg-success
|
||||
@elseif($invoice->status=='overdue') bg-danger
|
||||
@elseif($invoice->status=='pending') bg-warning text-dark
|
||||
@if($invoice->status == 'paid') bg-success
|
||||
@elseif($invoice->status == 'overdue') bg-danger
|
||||
@elseif($invoice->status == 'pending') bg-warning text-dark
|
||||
@else bg-secondary @endif">
|
||||
<i class="fas
|
||||
@if($invoice->status=='paid') fa-check-circle
|
||||
@elseif($invoice->status=='overdue') fa-exclamation-circle
|
||||
@elseif($invoice->status=='pending') fa-clock
|
||||
@if($invoice->status == 'paid') fa-check-circle
|
||||
@elseif($invoice->status == 'overdue') fa-exclamation-circle
|
||||
@elseif($invoice->status == 'pending') fa-clock
|
||||
@else fa-question-circle @endif me-1"></i>
|
||||
{{ ucfirst($invoice->status) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================
|
||||
ORDER & SHIPMENT ID BOXES
|
||||
============================ -->
|
||||
{{-- ID BOXES: INVOICE + CONTAINER --}}
|
||||
<div class="id-container">
|
||||
<div class="row">
|
||||
<!-- Order ID Box -->
|
||||
{{-- Invoice ID Box --}}
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="id-box id-box-primary">
|
||||
<div class="id-icon id-icon-primary">
|
||||
<i class="fas fa-shopping-cart"></i>
|
||||
<i class="fas fa-receipt"></i>
|
||||
</div>
|
||||
<div class="id-content">
|
||||
<div class="id-label">ORDER ID</div>
|
||||
<div class="id-label">Invoice ID</div>
|
||||
<div class="id-value">{{ $invoice->invoice_number }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Container ID Box --}}
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="id-box id-box-secondary">
|
||||
<div class="id-icon id-icon-secondary">
|
||||
<i class="fas fa-box"></i>
|
||||
</div>
|
||||
<div class="id-label">Container ID</div>
|
||||
<div class="id-value">
|
||||
@if($invoice->order && $invoice->order->order_id)
|
||||
{{ $invoice->order->order_id }}
|
||||
@elseif($invoice->order_id)
|
||||
{{ $invoice->order_id }}
|
||||
@if($invoice->container && $invoice->container->container_number)
|
||||
{{ $invoice->container->container_number }}
|
||||
@elseif($invoice->container_id)
|
||||
{{ $invoice->container_id }}
|
||||
@else
|
||||
N/A
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shipment ID Box -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="id-box id-box-secondary">
|
||||
<div class="id-icon id-icon-secondary">
|
||||
<i class="fas fa-shipping-fast"></i>
|
||||
|
||||
{{-- DATES --}}
|
||||
<div class="date-container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-5">
|
||||
<div class="date-card">
|
||||
<div class="date-icon">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
</div>
|
||||
<div class="id-content">
|
||||
<div class="id-label">SHIPMENT ID</div>
|
||||
<div class="id-value">
|
||||
@php
|
||||
$shipmentId = 'N/A';
|
||||
// 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 class="date-label">INVOICE DATE</div>
|
||||
<div class="date-value">
|
||||
{{ \Carbon\Carbon::parse($invoice->invoice_date)->format('M d, Y') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="date-connector">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="date-card">
|
||||
<div class="date-icon">
|
||||
<i class="fas fa-clock"></i>
|
||||
</div>
|
||||
<div class="date-label">DUE DATE</div>
|
||||
<div class="date-value @if($invoice->status == 'overdue') text-danger @endif">
|
||||
{{ \Carbon\Carbon::parse($invoice->due_date)->format('M d, Y') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================
|
||||
CUSTOMER DETAILS
|
||||
============================ -->
|
||||
{{-- CUSTOMER DETAILS --}}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0 fw-bold">
|
||||
@@ -495,9 +481,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================
|
||||
INVOICE ITEMS
|
||||
============================ -->
|
||||
{{-- INVOICE ITEMS (EDITABLE WHEN EMBEDDED) --}}
|
||||
@php
|
||||
$isEmbedded = isset($embedded) && $embedded;
|
||||
@endphp
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0 fw-bold">
|
||||
@@ -505,6 +493,13 @@
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
|
||||
@if($isEmbedded)
|
||||
<form action="{{ route('admin.invoices.items.update', $invoice->id) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
@endif
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
@@ -527,14 +522,41 @@
|
||||
<tbody>
|
||||
@foreach($invoice->items as $i => $item)
|
||||
<tr>
|
||||
<td class="text-center fw-bold text-muted">{{ $i+1 }}</td>
|
||||
<td class="text-center fw-bold text-muted">{{ $i + 1 }}</td>
|
||||
<td class="fw-semibold">{{ $item->description }}</td>
|
||||
<td class="text-center">{{ $item->ctn }}</td>
|
||||
<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>
|
||||
@@ -544,92 +566,127 @@
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
||||
@if($invoice->items->isEmpty())
|
||||
<tr>
|
||||
<td colspan="13" class="text-center text-muted fw-bold py-3">
|
||||
No invoice items found.
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if($isEmbedded)
|
||||
<div class="text-end p-3">
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-save me-1"></i> Update Items & Recalculate
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$totalAmount = $invoice->final_amount;
|
||||
$gstAmount = $invoice->gst_amount;
|
||||
$totalPayable = $invoice->final_amount_with_gst;
|
||||
|
||||
$paidAmount = $invoice->totalPaid();
|
||||
$remaining = $invoice->remainingAmount();
|
||||
@endphp
|
||||
|
||||
<div class="summary-container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
|
||||
<div class="amount-row">
|
||||
<span>Total Amount</span>
|
||||
<span class="fw-bold">₹{{ number_format($totalAmount,2) }}</span>
|
||||
{{-- FINAL SUMMARY --}}
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-6">
|
||||
<div class="card summary-card">
|
||||
<div class="card-header summary-header">
|
||||
<h6 class="mb-0 fw-bold">
|
||||
<i class="fas fa-calculator me-2"></i> Final Summary
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">Amount:</span>
|
||||
<span class="fw-bold text-dark">
|
||||
₹{{ number_format($invoice->final_amount, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="amount-row">
|
||||
<span>GST Amount</span>
|
||||
@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($gstAmount,2) }}
|
||||
₹{{ number_format($invoice->gst_amount / 2, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="amount-row border-top pt-2">
|
||||
<span class="fw-bold">Total Payable</span>
|
||||
{{-- SGST --}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">
|
||||
SGST ({{ $invoice->sgst_percent ?? ($invoice->gst_percent / 2) }}%):
|
||||
</span>
|
||||
<span class="fw-bold text-danger">
|
||||
₹{{ number_format($invoice->gst_amount / 2, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
@elseif($invoice->tax_type === 'igst')
|
||||
{{-- IGST --}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">
|
||||
IGST ({{ $invoice->igst_percent ?? $invoice->gst_percent }}%):
|
||||
</span>
|
||||
<span class="fw-bold text-danger">
|
||||
₹{{ number_format($invoice->gst_amount, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
@else
|
||||
{{-- Default GST --}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="fw-semibold">
|
||||
GST ({{ $invoice->gst_percent }}%):
|
||||
</span>
|
||||
<span class="fw-bold text-danger">
|
||||
₹{{ number_format($invoice->gst_amount, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center pt-1">
|
||||
<span class="fw-bold text-dark">Total Payable:</span>
|
||||
<span class="fw-bold text-success">
|
||||
₹{{ number_format($totalPayable,2) }}
|
||||
₹{{ number_format($invoice->final_amount_with_gst, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="amount-row">
|
||||
<span>Paid Amount</span>
|
||||
<span class="fw-bold text-primary">
|
||||
− ₹{{ number_format($paidAmount,2) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="amount-row border-top pt-2">
|
||||
<span class="fw-bold text-danger">Remaining Amount</span>
|
||||
<span class="fw-bold text-danger fs-5">
|
||||
₹{{ number_format($remaining,2) }}
|
||||
</span>
|
||||
{{-- FOOTER ACTIONS --}}
|
||||
<div class="mt-4 pt-3 border-top text-center">
|
||||
@if($invoice->pdf_path && $showActions)
|
||||
<a href="{{ asset($invoice->pdf_path) }}"
|
||||
class="btn btn-primary me-2" download>
|
||||
<i class="fas fa-download me-1"></i> Download PDF
|
||||
</a>
|
||||
<button class="btn btn-success" onclick="shareInvoice()">
|
||||
<i class="fas fa-share me-1"></i> Share
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- FOOTER MESSAGE --}}
|
||||
<div class="mt-4 pt-3 border-top text-center text-muted">
|
||||
<p class="mb-1">Thank you for your business!</p>
|
||||
<p class="mb-0">For any inquiries, contact us at support@Kent Logistic</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ============================
|
||||
FOOTER — DOWNLOAD & SHARE
|
||||
============================ -->
|
||||
<div class="mt-4 pt-3 border-top text-center">
|
||||
|
||||
<a href="{{ route('admin.invoices.download', $invoice->id) }}"
|
||||
class="btn btn-primary me-2">
|
||||
<i class="fas fa-download me-1"></i> Download PDF
|
||||
</a>
|
||||
|
||||
<button class="btn btn-success" onclick="shareInvoice()">
|
||||
<i class="fas fa-share me-1"></i> Share
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- ============================
|
||||
SHARE SCRIPT
|
||||
============================ -->
|
||||
<script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
function shareInvoice() {
|
||||
const shareData = {
|
||||
title: "Invoice {{ $invoice->invoice_number }}",
|
||||
text: "Sharing invoice {{ $invoice->invoice_number }}",
|
||||
url: "{{ route('admin.invoices.download', $invoice->id) }}"
|
||||
url: "{{ asset($invoice->pdf_path) }}"
|
||||
};
|
||||
|
||||
if (navigator.share) {
|
||||
@@ -639,7 +696,7 @@
|
||||
alert("Link copied! Sharing not supported on this browser.");
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -22,7 +22,7 @@
|
||||
.custom-table tbody tr:hover { background-color: #fffbea; transform: scale(1.01); box-shadow: 0 2px 6px rgba(0,0,0,0.05); }
|
||||
.priority-badge {
|
||||
display: inline-flex; align-items: center; font-size: 13.5px; padding: 6px 16px; border-radius: 12px; font-weight: 600;
|
||||
box-shadow: 0 1px 2px 0 rgba(130,130,130,0.15); width: 90px; min-height: 28px; justify-content: center;
|
||||
box-shadow: 0 1px 2px 0 rgba(230, 206, 206, 0.15); width: 90px; min-height: 28px; justify-content: center;
|
||||
color: #fff; margin: 2px 0; transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
.priority-badge:hover { transform: scale(1.08); }
|
||||
@@ -30,8 +30,8 @@
|
||||
.priority-medium { background: linear-gradient(135deg, #ffe390, #f5b041); }
|
||||
.priority-low { background: linear-gradient(135deg, #b8f0c2, #1d8660); }
|
||||
.custom-table thead th {
|
||||
text-align: center; font-weight: 700; color: #000; padding: 14px; font-size: 17px; letter-spacing: 0.5px;
|
||||
border-bottom: 2px solid #bfbfbf; background-color: #fde4b3;
|
||||
text-align: center; font-weight: 700; color: #ffffffff; padding: 14px; font-size: 17px; letter-spacing: 0.5px;
|
||||
border-bottom: 2px solid #bfbfbf; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);;
|
||||
}
|
||||
.custom-table thead tr:first-child th:first-child { border-top-left-radius: 12px; }
|
||||
.custom-table thead tr:first-child th:last-child { border-top-right-radius: 12px; }
|
||||
@@ -333,7 +333,11 @@ a.btn.btn-primary.position-relative .badge {
|
||||
box-shadow: 0 0 0 2px #ffffff;
|
||||
|
||||
}
|
||||
|
||||
.custom-table th,
|
||||
.custom-table td {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,15 @@ use App\Http\Controllers\MarkListController;
|
||||
use App\Http\Controllers\User\UserOrderController;
|
||||
use App\Http\Controllers\User\UserProfileController;
|
||||
use App\Http\Controllers\User\ChatController;
|
||||
<<<<<<< HEAD
|
||||
|
||||
|
||||
|
||||
Broadcast::routes(['middleware' => ['auth:api']]);
|
||||
|
||||
|
||||
=======
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
|
||||
//user send request
|
||||
Route::post('/signup-request', [RequestController::class, 'usersignup']);
|
||||
@@ -55,7 +64,11 @@ Route::middleware(['auth:api'])->group(function () {
|
||||
|
||||
// Route::post('/user/profile/update', [UserProfileController::class, 'updateProfile']);
|
||||
|
||||
<<<<<<< HEAD
|
||||
// ===========================
|
||||
=======
|
||||
// ===========================
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
// CHAT SUPPORT ROUTES
|
||||
// ===========================
|
||||
Route::get('/user/chat/start', [ChatController::class, 'startChat']);
|
||||
@@ -66,6 +79,15 @@ Route::middleware(['auth:api'])->group(function () {
|
||||
});
|
||||
|
||||
|
||||
<<<<<<< HEAD
|
||||
Route::post('/broadcasting/auth', function (Request $request) {
|
||||
if (!auth()->check()) {
|
||||
return response()->json(['message' => 'Unauthenticated'], 401);
|
||||
}
|
||||
|
||||
return Broadcast::auth($request);
|
||||
})->middleware('auth:api');
|
||||
=======
|
||||
|
||||
|
||||
|
||||
@@ -91,3 +113,4 @@ Route::post('/broadcasting/auth', function (Request $request) {
|
||||
|
||||
|
||||
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
|
||||
21
routes/broadcasting.php
Normal file
21
routes/broadcasting.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Broadcast::routes(['middleware' => ['web']]);
|
||||
|
||||
// Force admin guard authentication
|
||||
Route::post('/broadcasting/auth', function () {
|
||||
|
||||
if (auth('admin')->check()) {
|
||||
return Broadcast::auth(request());
|
||||
}
|
||||
|
||||
// Fallback check for normal users
|
||||
if (auth('web')->check()) {
|
||||
return Broadcast::auth(request());
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Unauthenticated'], 403);
|
||||
|
||||
})->middleware(['web']);
|
||||
@@ -2,77 +2,41 @@
|
||||
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
use App\Models\SupportTicket;
|
||||
use App\Models\Admin;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
file_put_contents(storage_path('logs/broadcast_debug.log'), now()." CHANNELS LOADED\n", FILE_APPEND);
|
||||
|
||||
Broadcast::routes([
|
||||
'middleware' => ['web', 'auth:admin'],
|
||||
]);
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Broadcast Channels
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
Broadcast::channel('ticket.{ticketId}', function ($user, $ticketId) {
|
||||
|
||||
try {
|
||||
// Very explicit logging to see what arrives here
|
||||
Log::info("CHANNEL AUTH CHECK (ENTER)", [
|
||||
'user_present' => $user !== null,
|
||||
'user_type' => is_object($user) ? get_class($user) : gettype($user),
|
||||
'user_id' => $user->id ?? null,
|
||||
\Log::info('🔐 Broadcasting Auth Check', [
|
||||
'ticketId' => $ticketId,
|
||||
'user_id' => $user->id ?? 'NULL',
|
||||
'user_table' => method_exists($user, 'getTable') ? $user->getTable() : 'unknown',
|
||||
'user_class' => get_class($user)
|
||||
]);
|
||||
|
||||
// Find ticket and log
|
||||
$ticket = SupportTicket::find($ticketId);
|
||||
Log::info("CHANNEL AUTH: found ticket", [
|
||||
'ticket_exists' => $ticket ? true : false,
|
||||
'ticket_id' => $ticket?->id,
|
||||
'ticket_user_id' => $ticket?->user_id,
|
||||
]);
|
||||
|
||||
if (! $ticket) {
|
||||
Log::warning("CHANNEL AUTH: ticket not found", ['ticketId' => $ticketId]);
|
||||
if (!$ticket) {
|
||||
\Log::warning('❌ Ticket not found', ['ticketId' => $ticketId]);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If admin, allow
|
||||
if ($user instanceof Admin) {
|
||||
Log::info("CHANNEL AUTH: admin allowed", ['admin_id' => $user->id]);
|
||||
// ✅ Admin/Staff Check (Session Auth)
|
||||
if (get_class($user) === 'App\Models\Admin') {
|
||||
\Log::info('✅ Admin authorized for ticket', ['admin_id' => $user->id]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If normal user, check ownership
|
||||
if (is_object($user) && isset($user->id)) {
|
||||
$allowed = $ticket->user_id === $user->id;
|
||||
Log::info("CHANNEL AUTH: user allowed check", [
|
||||
'ticket_user_id' => $ticket->user_id,
|
||||
'current_user_id' => $user->id,
|
||||
'allowed' => $allowed
|
||||
]);
|
||||
return $allowed;
|
||||
// ✅ User Check (JWT Auth - must own ticket)
|
||||
if (get_class($user) === 'App\Models\User' && $ticket->user_id === $user->id) {
|
||||
\Log::info('✅ User authorized for own ticket', ['user_id' => $user->id]);
|
||||
return true;
|
||||
}
|
||||
|
||||
Log::warning("CHANNEL AUTH: default deny");
|
||||
\Log::warning('❌ Authorization failed');
|
||||
return false;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("CHANNEL AUTH ERROR", [
|
||||
'message' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
Broadcast::channel('admin.chat', function ($admin) {
|
||||
return auth('admin')->check();
|
||||
});
|
||||
|
||||
// Broadcast::channel('ticket.{ticketId}', function ($admin, $ticketId) {
|
||||
// \Log::info('CHANNEL AUTH OK', [
|
||||
// 'admin_id' => $admin->id,
|
||||
// 'ticketId' => $ticketId,
|
||||
// ]);
|
||||
|
||||
// return true;
|
||||
// });
|
||||
54
routes/container.blade.php
Normal file
54
routes/container.blade.php
Normal file
@@ -0,0 +1,54 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Containers')
|
||||
|
||||
@section('content')
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Container List</h5>
|
||||
<a href="{{ route('containers.create') }}" class="btn btn-primary btn-sm">
|
||||
Add Container
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success">{{ session('success') }}</div>
|
||||
@endif
|
||||
|
||||
@if($containers->isEmpty())
|
||||
<p class="mb-0">No containers found.</p>
|
||||
@else
|
||||
<table class="table table-bordered table-striped align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Customer Name</th>
|
||||
<th>Container Number</th>
|
||||
<th>Date</th>
|
||||
<th>Excel File</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($containers as $container)
|
||||
<tr>
|
||||
<td>{{ $loop->iteration }}</td>
|
||||
<td>{{ $container->customer_name }}</td>
|
||||
<td>{{ $container->container_number }}</td>
|
||||
<td>{{ $container->container_date?->format('d-m-Y') }}</td>
|
||||
<td>
|
||||
@if($container->excel_file)
|
||||
<a href="{{ Storage::url($container->excel_file) }}" target="_blank">
|
||||
Download
|
||||
</a>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -24,6 +24,41 @@ Route::get('/', function () {
|
||||
return view('welcome');
|
||||
});
|
||||
|
||||
|
||||
|
||||
// ADD THIS BEFORE YOUR ADMIN ROUTES (Around Line 20-25)
|
||||
|
||||
// ==========================================
|
||||
// BROADCASTING AUTH (FOR ADMIN SESSION)
|
||||
// ==========================================
|
||||
Broadcast::routes(['middleware' => ['web']]);
|
||||
|
||||
// Custom broadcasting auth for admin guard
|
||||
Route::post('/broadcasting/auth', function (\Illuminate\Http\Request $request) {
|
||||
|
||||
\Log::info('🎯 Broadcasting Auth Request', [
|
||||
'channel' => $request->input('channel_name'),
|
||||
'admin_check' => auth('admin')->check(),
|
||||
'web_check' => auth('web')->check(),
|
||||
]);
|
||||
|
||||
// ✅ Admin Guard (Session)
|
||||
if (auth('admin')->check()) {
|
||||
\Log::info('✅ Admin authenticated', ['id' => auth('admin')->id()]);
|
||||
return Broadcast::auth($request);
|
||||
}
|
||||
|
||||
// ✅ Web Guard (Fallback for Users)
|
||||
if (auth('web')->check()) {
|
||||
\Log::info('✅ Web user authenticated', ['id' => auth('web')->id()]);
|
||||
return Broadcast::auth($request);
|
||||
}
|
||||
|
||||
\Log::warning('❌ No authentication found');
|
||||
return response()->json(['message' => 'Unauthenticated'], 403);
|
||||
|
||||
})->middleware('web');
|
||||
|
||||
// ---------------------------
|
||||
// ADMIN LOGIN ROUTES
|
||||
// ---------------------------
|
||||
@@ -41,7 +76,11 @@ Route::get('/login', function () {
|
||||
|
||||
|
||||
|
||||
//Broadcast::routes([
|
||||
//'middleware' => ['web', 'auth:admin'],
|
||||
//]);
|
||||
|
||||
Broadcast::routes(['middleware' => ['web']]);
|
||||
// ==========================================
|
||||
// PROTECTED ADMIN ROUTES (session protected)
|
||||
// ==========================================
|
||||
@@ -68,6 +107,33 @@ Route::prefix('admin')
|
||||
->name('admin.profile');
|
||||
|
||||
|
||||
|
||||
//---------------------------
|
||||
// CONTAINER ROUTES
|
||||
//---------------------------
|
||||
|
||||
Route::get('/containers', [ContainerController::class, 'index'])
|
||||
->name('containers.index');
|
||||
|
||||
// Container create form
|
||||
Route::get('/containers/create', [ContainerController::class, 'create'])
|
||||
->name('containers.create');
|
||||
|
||||
// Container store
|
||||
Route::post('/containers', [ContainerController::class, 'store'])
|
||||
->name('containers.store');
|
||||
|
||||
// Container delete
|
||||
Route::resource('containers', ContainerController::class);
|
||||
|
||||
//status update
|
||||
Route::post('containers/{container}/status', [ContainerController::class, 'updateStatus'])
|
||||
->name('containers.update-status');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ---------------------------
|
||||
// USER REQUESTS
|
||||
// ---------------------------
|
||||
@@ -239,6 +305,11 @@ Route::prefix('admin')
|
||||
->name('admin.invoice.installment.delete');
|
||||
|
||||
|
||||
Route::put('admin/invoices/{invoice}/items', [AdminInvoiceController::class, 'updateItems'])
|
||||
->name('admin.invoices.items.update');
|
||||
|
||||
|
||||
|
||||
// //Add New Invoice
|
||||
Route::get('/admin/invoices/create', [InvoiceController::class, 'create'])->name('admin.invoices.create');
|
||||
|
||||
@@ -355,3 +426,22 @@ Route::prefix('admin')
|
||||
// staff resource
|
||||
Route::resource('staff', AdminStaffController::class);
|
||||
});
|
||||
|
||||
// routes/web.php Line 57 (admin routes PREFIX ANTA)
|
||||
Route::prefix('admin')->middleware('auth:admin')->group(function () {
|
||||
// ... your routes
|
||||
});
|
||||
|
||||
Route::post('/admin/broadcasting/auth', function () {
|
||||
return Broadcast::auth(request());
|
||||
})->middleware('auth:admin');
|
||||
|
||||
|
||||
// Container row update route
|
||||
Route::post('/containers/{container}/update-rows', [ContainerController::class, 'updateRows'])
|
||||
->name('containers.rows.update');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,9 +8,8 @@ export default defineConfig({
|
||||
input: [
|
||||
"resources/css/app.css",
|
||||
"resources/js/app.js",
|
||||
"resources/js/echo.js",
|
||||
// ❌ Remove echo.js from here - it's imported in app.js
|
||||
],
|
||||
|
||||
refresh: true,
|
||||
}),
|
||||
tailwindcss(),
|
||||
|
||||
Reference in New Issue
Block a user