All Kent Code Updated
This commit is contained in:
12
.env.example
12
.env.example
@@ -20,12 +20,12 @@ LOG_STACK=single
|
|||||||
LOG_DEPRECATIONS_CHANNEL=null
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
DB_CONNECTION=sqlite
|
DB_CONNECTION=mysql
|
||||||
# DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
# DB_PORT=3306
|
DB_PORT=3306
|
||||||
# DB_DATABASE=laravel
|
DB_DATABASE=kent_logistics6
|
||||||
# DB_USERNAME=root
|
DB_USERNAME=root
|
||||||
# DB_PASSWORD=
|
DB_PASSWORD=
|
||||||
|
|
||||||
SESSION_DRIVER=database
|
SESSION_DRIVER=database
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
|
|||||||
63
app/Exports/InvoicesExport.php
Normal file
63
app/Exports/InvoicesExport.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromView;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class InvoicesExport implements FromView
|
||||||
|
{
|
||||||
|
protected $request;
|
||||||
|
|
||||||
|
public function __construct(Request $request)
|
||||||
|
{
|
||||||
|
$this->request = $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(): View
|
||||||
|
{
|
||||||
|
$request = $this->request;
|
||||||
|
|
||||||
|
$invoices = DB::table('invoices')
|
||||||
|
->leftJoin('containers', 'containers.id', '=', 'invoices.container_id')
|
||||||
|
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
|
||||||
|
->select(
|
||||||
|
'invoices.invoice_number',
|
||||||
|
'invoices.invoice_date',
|
||||||
|
'invoices.mark_no',
|
||||||
|
'containers.container_number',
|
||||||
|
'containers.container_date',
|
||||||
|
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
|
||||||
|
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name'),
|
||||||
|
'invoices.final_amount',
|
||||||
|
'invoices.final_amount_with_gst',
|
||||||
|
'invoices.status as invoice_status'
|
||||||
|
)
|
||||||
|
->when($request->filled('search'), function ($q) use ($request) {
|
||||||
|
$search = trim($request->search);
|
||||||
|
$q->where(function ($qq) use ($search) {
|
||||||
|
$qq->where('invoices.invoice_number', 'like', "%{$search}%")
|
||||||
|
->orWhere('invoices.mark_no', 'like', "%{$search}%")
|
||||||
|
->orWhere('containers.container_number', 'like', "%{$search}%")
|
||||||
|
->orWhere('mark_list.company_name', 'like', "%{$search}%")
|
||||||
|
->orWhere('mark_list.customer_name', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->when($request->filled('status'), function ($q) use ($request) {
|
||||||
|
$q->where('invoices.status', $request->status);
|
||||||
|
})
|
||||||
|
->when($request->filled('from_date'), function ($q) use ($request) {
|
||||||
|
$q->whereDate('invoices.invoice_date', '>=', $request->from_date);
|
||||||
|
})
|
||||||
|
->when($request->filled('to_date'), function ($q) use ($request) {
|
||||||
|
$q->whereDate('invoices.invoice_date', '<=', $request->to_date);
|
||||||
|
})
|
||||||
|
->orderByDesc('containers.container_date')
|
||||||
|
->orderByDesc('invoices.id')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('admin.pdf.invoices_excel', compact('invoices'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,21 +3,51 @@
|
|||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
use App\Models\InvoiceItem;
|
use App\Models\InvoiceItem;
|
||||||
use Mpdf\Mpdf;
|
|
||||||
use App\Models\InvoiceInstallment;
|
use App\Models\InvoiceInstallment;
|
||||||
|
use App\Models\InvoiceChargeGroup;
|
||||||
|
use App\Models\InvoiceChargeGroupItem;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Mpdf\Mpdf;
|
||||||
|
|
||||||
class AdminInvoiceController extends Controller
|
class AdminInvoiceController extends Controller
|
||||||
{
|
{
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// INVOICE LIST PAGE
|
// INVOICE LIST PAGE
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
public function index()
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$invoices = Invoice::with(['order.shipments'])->latest()->get();
|
$query = Invoice::with(['items', 'customer', 'container']);
|
||||||
|
|
||||||
|
// Search (जर पुढे form मधून पाठवलंस तर)
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$search = $request->search;
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('invoice_number', 'like', "%{$search}%")
|
||||||
|
->orWhere('customer_name', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status filter
|
||||||
|
if ($request->filled('status') && $request->status !== 'all') {
|
||||||
|
$query->where('status', $request->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date range filter (invoice_date वर)
|
||||||
|
if ($request->filled('start_date')) {
|
||||||
|
$query->whereDate('invoice_date', '>=', $request->start_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('end_date')) {
|
||||||
|
$query->whereDate('invoice_date', '<=', $request->end_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Latest first
|
||||||
|
$invoices = $query->latest()->get();
|
||||||
|
|
||||||
return view('admin.invoice', compact('invoices'));
|
return view('admin.invoice', compact('invoices'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,11 +56,14 @@ class AdminInvoiceController extends Controller
|
|||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
public function popup($id)
|
public function popup($id)
|
||||||
{
|
{
|
||||||
$invoice = Invoice::with(['items', 'order', 'installments'])->findOrFail($id);
|
$invoice = Invoice::with([
|
||||||
|
'items',
|
||||||
|
'customer',
|
||||||
|
'container',
|
||||||
|
'chargeGroups.items.item',
|
||||||
|
])->findOrFail($id);
|
||||||
|
|
||||||
$shipment = \App\Models\Shipment::whereHas('items', function ($q) use ($invoice) {
|
$shipment = null;
|
||||||
$q->where('order_id', $invoice->order_id);
|
|
||||||
})->first();
|
|
||||||
|
|
||||||
return view('admin.popup_invoice', compact('invoice', 'shipment'));
|
return view('admin.popup_invoice', compact('invoice', 'shipment'));
|
||||||
}
|
}
|
||||||
@@ -40,20 +73,34 @@ class AdminInvoiceController extends Controller
|
|||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
public function edit($id)
|
public function edit($id)
|
||||||
{
|
{
|
||||||
$invoice = Invoice::with(['order.shipments'])->findOrFail($id);
|
$invoice = Invoice::with([
|
||||||
$shipment = $invoice->order?->shipments?->first();
|
'items',
|
||||||
|
'customer',
|
||||||
|
'container',
|
||||||
|
'chargeGroups.items',
|
||||||
|
])->findOrFail($id);
|
||||||
|
|
||||||
return view('admin.invoice_edit', compact('invoice', 'shipment'));
|
$shipment = null;
|
||||||
|
|
||||||
|
$groupedItemIds = $invoice->chargeGroups
|
||||||
|
->flatMap(function ($group) {
|
||||||
|
return $group->items->pluck('invoice_item_id');
|
||||||
|
})
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
return view('admin.invoice_edit', compact('invoice', 'shipment', 'groupedItemIds'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// UPDATE INVOICE
|
// UPDATE INVOICE (HEADER LEVEL)
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
public function update(Request $request, $id)
|
public function update(Request $request, $id)
|
||||||
{
|
{
|
||||||
Log::info("🟡 Invoice Update Request Received", [
|
Log::info('🟡 Invoice Update Request Received', [
|
||||||
'invoice_id' => $id,
|
'invoice_id' => $id,
|
||||||
'request' => $request->all()
|
'request' => $request->all(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$invoice = Invoice::findOrFail($id);
|
$invoice = Invoice::findOrFail($id);
|
||||||
@@ -68,31 +115,31 @@ class AdminInvoiceController extends Controller
|
|||||||
'notes' => 'nullable|string',
|
'notes' => 'nullable|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Log::info("✅ Validated Invoice Update Data", $data);
|
Log::info('✅ Validated Invoice Update Data', $data);
|
||||||
|
|
||||||
$finalAmount = floatval($data['final_amount']);
|
$finalAmount = (float) $data['final_amount'];
|
||||||
$taxPercent = floatval($data['tax_percent']);
|
$taxPercent = (float) $data['tax_percent'];
|
||||||
$taxAmount = 0;
|
|
||||||
|
|
||||||
if ($data['tax_type'] === 'gst') {
|
if ($data['tax_type'] === 'gst') {
|
||||||
Log::info("🟢 GST Selected", compact('taxPercent'));
|
Log::info('🟢 GST Selected', compact('taxPercent'));
|
||||||
|
|
||||||
$data['cgst_percent'] = $taxPercent / 2;
|
$data['cgst_percent'] = $taxPercent / 2;
|
||||||
$data['sgst_percent'] = $taxPercent / 2;
|
$data['sgst_percent'] = $taxPercent / 2;
|
||||||
$data['igst_percent'] = 0;
|
$data['igst_percent'] = 0;
|
||||||
} else {
|
} else {
|
||||||
Log::info("🔵 IGST Selected", compact('taxPercent'));
|
Log::info('🔵 IGST Selected', compact('taxPercent'));
|
||||||
|
|
||||||
$data['cgst_percent'] = 0;
|
$data['cgst_percent'] = 0;
|
||||||
$data['sgst_percent'] = 0;
|
$data['sgst_percent'] = 0;
|
||||||
$data['igst_percent'] = $taxPercent;
|
$data['igst_percent'] = $taxPercent;
|
||||||
}
|
}
|
||||||
|
|
||||||
$taxAmount = ($finalAmount * $taxPercent) / 100;
|
$gstAmount = ($finalAmount * $taxPercent) / 100;
|
||||||
|
$data['gst_amount'] = $gstAmount;
|
||||||
$data['gst_amount'] = $taxAmount;
|
$data['final_amount_with_gst'] = $finalAmount + $gstAmount;
|
||||||
$data['final_amount_with_gst'] = $finalAmount + $taxAmount;
|
|
||||||
$data['gst_percent'] = $taxPercent;
|
$data['gst_percent'] = $taxPercent;
|
||||||
|
|
||||||
Log::info("📌 Final Calculated Invoice Values", [
|
Log::info('📌 Final Calculated Invoice Values', [
|
||||||
'invoice_id' => $invoice->id,
|
'invoice_id' => $invoice->id,
|
||||||
'final_amount' => $finalAmount,
|
'final_amount' => $finalAmount,
|
||||||
'gst_amount' => $data['gst_amount'],
|
'gst_amount' => $data['gst_amount'],
|
||||||
@@ -105,11 +152,23 @@ class AdminInvoiceController extends Controller
|
|||||||
|
|
||||||
$invoice->update($data);
|
$invoice->update($data);
|
||||||
|
|
||||||
Log::info("✅ Invoice Updated Successfully", [
|
Log::info('✅ Invoice Updated Successfully', [
|
||||||
'invoice_id' => $invoice->id
|
'invoice_id' => $invoice->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$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,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// regenerate PDF
|
|
||||||
$this->generateInvoicePDF($invoice);
|
$this->generateInvoicePDF($invoice);
|
||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
@@ -117,13 +176,88 @@ class AdminInvoiceController extends Controller
|
|||||||
->with('success', 'Invoice updated & PDF generated successfully.');
|
->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
|
// PDF GENERATION USING mPDF
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
public function generateInvoicePDF($invoice)
|
public function generateInvoicePDF($invoice)
|
||||||
{
|
{
|
||||||
$invoice->load(['items', 'order.shipments']);
|
$invoice->load(['items', 'customer', 'container']);
|
||||||
$shipment = $invoice->order?->shipments?->first();
|
$shipment = null;
|
||||||
|
|
||||||
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
|
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
|
||||||
$folder = public_path('invoices/');
|
$folder = public_path('invoices/');
|
||||||
|
|
||||||
@@ -132,35 +266,30 @@ class AdminInvoiceController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$filePath = $folder . $fileName;
|
$filePath = $folder . $fileName;
|
||||||
|
|
||||||
if (file_exists($filePath)) {
|
if (file_exists($filePath)) {
|
||||||
unlink($filePath);
|
unlink($filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
$mpdf = new Mpdf(['mode' => 'utf-8', 'format' => 'A4', 'default_font' => 'sans-serif']);
|
$mpdf = new Mpdf([
|
||||||
$html = view('admin.pdf.invoice', ['invoice' => $invoice, 'shipment' => $shipment])->render();
|
'mode' => 'utf-8',
|
||||||
|
'format' => 'A4',
|
||||||
|
'default_font' => 'sans-serif',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$html = view('admin.pdf.invoice', [
|
||||||
|
'invoice' => $invoice,
|
||||||
|
'shipment' => $shipment,
|
||||||
|
])->render();
|
||||||
|
|
||||||
$mpdf->WriteHTML($html);
|
$mpdf->WriteHTML($html);
|
||||||
$mpdf->Output($filePath, 'F');
|
$mpdf->Output($filePath, 'F');
|
||||||
|
|
||||||
$invoice->update(['pdf_path' => 'invoices/' . $fileName]);
|
$invoice->update(['pdf_path' => 'invoices/' . $fileName]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function downloadInvoice($id)
|
|
||||||
{
|
|
||||||
$invoice = Invoice::findOrFail($id);
|
|
||||||
|
|
||||||
// Generate PDF if missing
|
|
||||||
if (
|
|
||||||
!$invoice->pdf_path ||
|
|
||||||
!file_exists(public_path($invoice->pdf_path))
|
|
||||||
) {
|
|
||||||
$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)
|
public function storeInstallment(Request $request, $invoice_id)
|
||||||
{
|
{
|
||||||
@@ -172,15 +301,13 @@ class AdminInvoiceController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$invoice = Invoice::findOrFail($invoice_id);
|
$invoice = Invoice::findOrFail($invoice_id);
|
||||||
|
|
||||||
$paidTotal = $invoice->installments()->sum('amount');
|
$paidTotal = $invoice->installments()->sum('amount');
|
||||||
// Use GST-inclusive total for all calculations/checks
|
|
||||||
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
||||||
|
|
||||||
if ($request->amount > $remaining) {
|
if ($request->amount > $remaining) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Installment amount exceeds remaining balance.'
|
'message' => 'Installment amount exceeds remaining balance.',
|
||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,11 +321,8 @@ class AdminInvoiceController extends Controller
|
|||||||
|
|
||||||
$newPaid = $paidTotal + $request->amount;
|
$newPaid = $paidTotal + $request->amount;
|
||||||
|
|
||||||
// Mark as 'paid' if GST-inclusive total is cleared
|
|
||||||
if ($newPaid >= $invoice->final_amount_with_gst) {
|
if ($newPaid >= $invoice->final_amount_with_gst) {
|
||||||
$invoice->update(['status' => 'paid']);
|
$invoice->update(['status' => 'paid']);
|
||||||
|
|
||||||
$this->generateInvoicePDF($invoice);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@@ -210,9 +334,13 @@ class AdminInvoiceController extends Controller
|
|||||||
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
||||||
'baseAmount' => $invoice->final_amount,
|
'baseAmount' => $invoice->final_amount,
|
||||||
'remaining' => max(0, $invoice->final_amount_with_gst - $newPaid),
|
'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)
|
public function deleteInstallment($id)
|
||||||
{
|
{
|
||||||
$installment = InvoiceInstallment::findOrFail($id);
|
$installment = InvoiceInstallment::findOrFail($id);
|
||||||
@@ -223,11 +351,8 @@ class AdminInvoiceController extends Controller
|
|||||||
$paidTotal = $invoice->installments()->sum('amount');
|
$paidTotal = $invoice->installments()->sum('amount');
|
||||||
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
$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']);
|
$invoice->update(['status' => 'pending']);
|
||||||
|
|
||||||
$this->generateInvoicePDF($invoice);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@@ -238,8 +363,54 @@ class AdminInvoiceController extends Controller
|
|||||||
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
||||||
'baseAmount' => $invoice->final_amount,
|
'baseAmount' => $invoice->final_amount,
|
||||||
'remaining' => $remaining,
|
'remaining' => $remaining,
|
||||||
'isZero' => $paidTotal == 0
|
'isZero' => $paidTotal == 0,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// CHARGE GROUP SAVE (NEW)
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
public function storeChargeGroup(Request $request, $invoiceId)
|
||||||
|
{
|
||||||
|
$invoice = Invoice::with('items')->findOrFail($invoiceId);
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'group_name' => 'nullable|string|max:255',
|
||||||
|
'basis_type' => 'required|in:ttl_qty,amount,ttl_cbm,ttl_kg',
|
||||||
|
'basis_value' => 'required|numeric',
|
||||||
|
'rate' => 'required|numeric|min:0.0001',
|
||||||
|
'auto_total' => 'required|numeric|min:0.01',
|
||||||
|
'item_ids' => 'required|array',
|
||||||
|
'item_ids.*' => 'integer|exists:invoice_items,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$group = InvoiceChargeGroup::create([
|
||||||
|
'invoice_id' => $invoice->id,
|
||||||
|
'group_name' => $data['group_name'] ?? null,
|
||||||
|
'basis_type' => $data['basis_type'],
|
||||||
|
'basis_value' => $data['basis_value'],
|
||||||
|
'rate' => $data['rate'],
|
||||||
|
'total_charge' => $data['auto_total'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($data['item_ids'] as $itemId) {
|
||||||
|
InvoiceChargeGroupItem::create([
|
||||||
|
'group_id' => $group->id,
|
||||||
|
'invoice_item_id' => $itemId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->back()
|
||||||
|
->with('success', 'Charge group saved successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function download(Invoice $invoice)
|
||||||
|
{
|
||||||
|
if (!$invoice->pdf_path || !Storage::exists($invoice->pdf_path)) {
|
||||||
|
return back()->with('error', 'PDF not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Storage::download($invoice->pdf_path, $invoice->invoice_number . '.pdf');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,26 +10,89 @@ use App\Models\MarkList;
|
|||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
use App\Models\InvoiceItem;
|
use App\Models\InvoiceItem;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Models\Container;
|
||||||
|
use App\Models\Admin;
|
||||||
|
use App\Models\Shipment;
|
||||||
use PDF;
|
use PDF;
|
||||||
use Maatwebsite\Excel\Facades\Excel;
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
use App\Exports\OrdersExport;
|
use App\Exports\OrdersExport;
|
||||||
use App\Imports\OrderItemsPreviewImport;
|
use App\Imports\OrderItemsPreviewImport;
|
||||||
|
|
||||||
|
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Exports\InvoicesExport;
|
||||||
|
|
||||||
class AdminOrderController extends Controller
|
class AdminOrderController extends Controller
|
||||||
{
|
{
|
||||||
/* ---------------------------
|
/* ---------------------------
|
||||||
* LIST / DASHBOARD
|
* DASHBOARD (old UI: stats + recent orders)
|
||||||
* ---------------------------*/
|
* ---------------------------*/
|
||||||
public function index()
|
public function dashboard()
|
||||||
{
|
{
|
||||||
$orders = Order::latest()->get();
|
$totalOrders = Order::count();
|
||||||
$markList = MarkList::where('status', 'active')->get();
|
$pendingOrders = Order::where('status', 'pending')->count();
|
||||||
|
$totalShipments = Shipment::count();
|
||||||
|
$totalItems = OrderItem::count();
|
||||||
|
$totalRevenue = Invoice::sum('final_amount_with_gst');
|
||||||
|
$activeCustomers = User::where('status', 'active')->count();
|
||||||
|
$inactiveCustomers = User::where('status', 'inactive')->count();
|
||||||
|
$totalStaff = Admin::where('type', 'staff')->count();
|
||||||
|
|
||||||
return view('admin.dashboard', compact('orders', 'markList'));
|
$markList = MarkList::where('status', 'active')->get();
|
||||||
|
$orders = Order::latest()->get();
|
||||||
|
|
||||||
|
return view('admin.dashboard', compact(
|
||||||
|
'totalOrders',
|
||||||
|
'pendingOrders',
|
||||||
|
'totalShipments',
|
||||||
|
'totalItems',
|
||||||
|
'totalRevenue',
|
||||||
|
'activeCustomers',
|
||||||
|
'inactiveCustomers',
|
||||||
|
'totalStaff',
|
||||||
|
'orders',
|
||||||
|
'markList'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------
|
||||||
|
* LIST (new: Invoices Management for Orders page)
|
||||||
|
* ---------------------------*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$invoices = DB::table('invoices')
|
||||||
|
->leftJoin('containers', 'containers.id', '=', 'invoices.container_id')
|
||||||
|
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
|
||||||
|
->select(
|
||||||
|
'invoices.id',
|
||||||
|
'invoices.invoice_number',
|
||||||
|
'invoices.invoice_date',
|
||||||
|
'invoices.final_amount',
|
||||||
|
'invoices.final_amount_with_gst',
|
||||||
|
'invoices.status as invoice_status',
|
||||||
|
'invoices.mark_no',
|
||||||
|
'containers.container_number',
|
||||||
|
'containers.container_date',
|
||||||
|
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
|
||||||
|
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name')
|
||||||
|
)
|
||||||
|
->when($request->filled('search'), function ($q) use ($request) {
|
||||||
|
$search = trim($request->search);
|
||||||
|
$q->where(function ($qq) use ($search) {
|
||||||
|
$qq->where('invoices.invoice_number', 'like', "%{$search}%")
|
||||||
|
->orWhere('containers.container_number', 'like', "%{$search}%")
|
||||||
|
->orWhere('invoices.mark_no', 'like', "%{$search}%")
|
||||||
|
->orWhere('mark_list.company_name', 'like', "%{$search}%")
|
||||||
|
->orWhere('mark_list.customer_name', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->when($request->filled('status'), function ($q) use ($request) {
|
||||||
|
$q->where('invoices.status', $request->status);
|
||||||
|
})
|
||||||
|
->orderByDesc('invoices.invoice_date') // इथे बदल
|
||||||
|
->orderByDesc('invoices.id') // same-date साठी tie-breaker
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('admin.orders', compact('invoices'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------
|
/* ---------------------------
|
||||||
@@ -82,7 +145,6 @@ class AdminOrderController extends Controller
|
|||||||
'shop_no' => 'nullable|string',
|
'shop_no' => 'nullable|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ✅ BACKEND CALCULATION
|
|
||||||
$ctn = (float) ($data['ctn'] ?? 0);
|
$ctn = (float) ($data['ctn'] ?? 0);
|
||||||
$qty = (float) ($data['qty'] ?? 0);
|
$qty = (float) ($data['qty'] ?? 0);
|
||||||
$price = (float) ($data['price'] ?? 0);
|
$price = (float) ($data['price'] ?? 0);
|
||||||
@@ -99,12 +161,10 @@ class AdminOrderController extends Controller
|
|||||||
OrderItem::create($data);
|
OrderItem::create($data);
|
||||||
|
|
||||||
$this->recalcTotals($order);
|
$this->recalcTotals($order);
|
||||||
$this->updateInvoiceFromOrder($order);
|
|
||||||
|
|
||||||
return redirect()->back()->with('success', 'Item added and totals updated.');
|
return redirect()->back()->with('success', 'Item added and totals updated.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function deleteItem($id)
|
public function deleteItem($id)
|
||||||
{
|
{
|
||||||
$item = OrderItem::findOrFail($id);
|
$item = OrderItem::findOrFail($id);
|
||||||
@@ -113,7 +173,6 @@ class AdminOrderController extends Controller
|
|||||||
$item->delete();
|
$item->delete();
|
||||||
|
|
||||||
$this->recalcTotals($order);
|
$this->recalcTotals($order);
|
||||||
$this->updateInvoiceFromOrder($order);
|
|
||||||
|
|
||||||
return redirect()->back()->with('success', 'Item deleted and totals updated.');
|
return redirect()->back()->with('success', 'Item deleted and totals updated.');
|
||||||
}
|
}
|
||||||
@@ -126,7 +185,6 @@ class AdminOrderController extends Controller
|
|||||||
$item->restore();
|
$item->restore();
|
||||||
|
|
||||||
$this->recalcTotals($order);
|
$this->recalcTotals($order);
|
||||||
$this->updateInvoiceFromOrder($order);
|
|
||||||
|
|
||||||
return redirect()->back()->with('success', 'Item restored and totals updated.');
|
return redirect()->back()->with('success', 'Item restored and totals updated.');
|
||||||
}
|
}
|
||||||
@@ -285,7 +343,6 @@ class AdminOrderController extends Controller
|
|||||||
$order = Order::with([
|
$order = Order::with([
|
||||||
'markList',
|
'markList',
|
||||||
'items',
|
'items',
|
||||||
'invoice.items',
|
|
||||||
'shipments' => function ($q) use ($id) {
|
'shipments' => function ($q) use ($id) {
|
||||||
$q->whereHas('orders', function ($oq) use ($id) {
|
$q->whereHas('orders', function ($oq) use ($id) {
|
||||||
$oq->where('orders.id', $id)
|
$oq->where('orders.id', $id)
|
||||||
@@ -365,29 +422,6 @@ class AdminOrderController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$invoiceData = null;
|
$invoiceData = null;
|
||||||
if ($order->invoice) {
|
|
||||||
$invoice = $order->invoice;
|
|
||||||
$invoiceData = [
|
|
||||||
'invoice_no' => $invoice->invoice_number,
|
|
||||||
'status' => $invoice->status,
|
|
||||||
'invoice_date' => $invoice->invoice_date,
|
|
||||||
'due_date' => $invoice->due_date,
|
|
||||||
'customer' => [
|
|
||||||
'name' => $invoice->customer_name,
|
|
||||||
'mobile' => $invoice->customer_mobile,
|
|
||||||
'email' => $invoice->customer_email,
|
|
||||||
'address' => $invoice->customer_address,
|
|
||||||
'pincode' => $invoice->pincode,
|
|
||||||
],
|
|
||||||
'items' => $invoice->items,
|
|
||||||
'summary' => [
|
|
||||||
'amount' => $invoice->final_amount,
|
|
||||||
'cgst' => 0,
|
|
||||||
'sgst' => 0,
|
|
||||||
'total' => $invoice->final_amount_with_gst,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('admin.see_order', compact(
|
return view('admin.see_order', compact(
|
||||||
'order',
|
'order',
|
||||||
@@ -398,14 +432,13 @@ class AdminOrderController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------
|
/* ---------------------------
|
||||||
* FILTERED LIST + EXPORTS
|
* FILTERED LIST + EXPORTS (old orders listing)
|
||||||
* ---------------------------*/
|
* ---------------------------*/
|
||||||
public function orderShow()
|
public function orderShow()
|
||||||
{
|
{
|
||||||
$orders = Order::with([
|
$orders = Order::with([
|
||||||
'markList',
|
'markList',
|
||||||
'shipments',
|
'shipments',
|
||||||
'invoice'
|
|
||||||
])->latest('id')->get();
|
])->latest('id')->get();
|
||||||
|
|
||||||
return view('admin.orders', compact('orders'));
|
return view('admin.orders', compact('orders'));
|
||||||
@@ -414,7 +447,7 @@ class AdminOrderController extends Controller
|
|||||||
private function buildOrdersQueryFromRequest(Request $request)
|
private function buildOrdersQueryFromRequest(Request $request)
|
||||||
{
|
{
|
||||||
$query = Order::query()
|
$query = Order::query()
|
||||||
->with(['markList', 'invoice', 'shipments']);
|
->with(['markList', 'shipments']);
|
||||||
|
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
$search = trim($request->search);
|
$search = trim($request->search);
|
||||||
@@ -427,23 +460,12 @@ class AdminOrderController extends Controller
|
|||||||
->orWhere('origin', 'like', "%{$search}%")
|
->orWhere('origin', 'like', "%{$search}%")
|
||||||
->orWhere('destination', 'like', "%{$search}%");
|
->orWhere('destination', 'like', "%{$search}%");
|
||||||
})
|
})
|
||||||
->orWhereHas('invoice', function ($q3) use ($search) {
|
|
||||||
$q3->where('invoice_number', 'like', "%{$search}%");
|
|
||||||
})
|
|
||||||
->orWhereHas('shipments', function ($q4) use ($search) {
|
->orWhereHas('shipments', function ($q4) use ($search) {
|
||||||
$q4->where('shipments.shipment_id', 'like', "%{$search}%");
|
$q4->where('shipments.shipment_id', 'like', "%{$search}%");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('status')) {
|
|
||||||
$query->where(function ($q) use ($request) {
|
|
||||||
$q->whereHas('invoice', function ($q2) use ($request) {
|
|
||||||
$q2->where('status', $request->status);
|
|
||||||
})->orWhereDoesntHave('invoice');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->filled('shipment')) {
|
if ($request->filled('shipment')) {
|
||||||
$query->where(function ($q) use ($request) {
|
$query->where(function ($q) use ($request) {
|
||||||
$q->whereHas('shipments', function ($q2) use ($request) {
|
$q->whereHas('shipments', function ($q2) use ($request) {
|
||||||
@@ -465,62 +487,83 @@ class AdminOrderController extends Controller
|
|||||||
|
|
||||||
public function downloadPdf(Request $request)
|
public function downloadPdf(Request $request)
|
||||||
{
|
{
|
||||||
$orders = $this->buildOrdersQueryFromRequest($request)->get();
|
$invoices = DB::table('invoices')
|
||||||
$filters = [
|
->leftJoin('containers', 'containers.id', '=', 'invoices.container_id')
|
||||||
'search' => $request->search,
|
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
|
||||||
'status' => $request->status,
|
->select(
|
||||||
'shipment' => $request->shipment,
|
'invoices.invoice_number',
|
||||||
'from' => $request->from_date,
|
'invoices.invoice_date',
|
||||||
'to' => $request->to_date,
|
'invoices.mark_no',
|
||||||
];
|
'containers.container_number',
|
||||||
|
'containers.container_date',
|
||||||
|
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
|
||||||
|
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name'),
|
||||||
|
'invoices.final_amount',
|
||||||
|
'invoices.final_amount_with_gst',
|
||||||
|
'invoices.status as invoice_status'
|
||||||
|
)
|
||||||
|
->when($request->filled('search'), function ($q) use ($request) {
|
||||||
|
$search = trim($request->search);
|
||||||
|
$q->where(function ($qq) use ($search) {
|
||||||
|
$qq->where('invoices.invoice_number', 'like', "%{$search}%")
|
||||||
|
->orWhere('invoices.mark_no', 'like', "%{$search}%")
|
||||||
|
->orWhere('containers.container_number', 'like', "%{$search}%")
|
||||||
|
->orWhere('mark_list.company_name', 'like', "%{$search}%")
|
||||||
|
->orWhere('mark_list.customer_name', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->when($request->filled('status'), function ($q) use ($request) {
|
||||||
|
$q->where('invoices.status', $request->status);
|
||||||
|
})
|
||||||
|
->when($request->filled('from_date'), function ($q) use ($request) {
|
||||||
|
$q->whereDate('invoices.invoice_date', '>=', $request->from_date);
|
||||||
|
})
|
||||||
|
->when($request->filled('to_date'), function ($q) use ($request) {
|
||||||
|
$q->whereDate('invoices.invoice_date', '<=', $request->to_date);
|
||||||
|
})
|
||||||
|
->orderByDesc('containers.container_date')
|
||||||
|
->orderByDesc('invoices.id')
|
||||||
|
->get();
|
||||||
|
|
||||||
$pdf = PDF::loadView('admin.orders.pdf', compact('orders', 'filters'))
|
$pdf = PDF::loadView('admin.pdf.invoices_report', compact('invoices'))
|
||||||
->setPaper('a4', 'landscape');
|
->setPaper('a4', 'landscape');
|
||||||
|
|
||||||
return $pdf->download(
|
return $pdf->download(
|
||||||
'orders-report-' . now()->format('Y-m-d') . '.pdf'
|
'invoices-report-' . now()->format('Y-m-d') . '.pdf'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function downloadExcel(Request $request)
|
public function downloadExcel(Request $request)
|
||||||
{
|
{
|
||||||
return Excel::download(
|
return Excel::download(
|
||||||
new OrdersExport($request),
|
new InvoicesExport($request),
|
||||||
'orders-report-' . now()->format('Y-m-d') . '.xlsx'
|
'invoices-report-' . now()->format('Y-m-d') . '.xlsx'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------
|
/* --------------------------------------------------
|
||||||
* NEW: Create Order + Invoice directly from popup
|
* NEW: Create Order + Invoice directly from popup
|
||||||
* route: admin.orders.temp.add (Create New Order form)
|
|
||||||
* --------------------------------------------------*/
|
* --------------------------------------------------*/
|
||||||
public function addTempItem(Request $request)
|
public function addTempItem(Request $request)
|
||||||
{
|
{
|
||||||
// 1) order-level fields
|
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'mark_no' => 'required',
|
'mark_no' => 'required',
|
||||||
'origin' => 'required',
|
'origin' => 'nullable',
|
||||||
'destination' => 'required',
|
'destination' => 'nullable',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 2) multi-row items
|
|
||||||
$items = $request->validate([
|
$items = $request->validate([
|
||||||
'items' => 'required|array',
|
'items' => 'required|array',
|
||||||
'items.*.description' => 'required|string',
|
'items.*.description' => 'required|string',
|
||||||
'items.*.ctn' => 'nullable|numeric',
|
'items.*.ctn' => 'nullable|numeric',
|
||||||
'items.*.qty' => 'nullable|numeric',
|
'items.*.qty' => 'nullable|numeric',
|
||||||
|
|
||||||
'items.*.unit' => 'nullable|string',
|
'items.*.unit' => 'nullable|string',
|
||||||
'items.*.price' => 'nullable|numeric',
|
'items.*.price' => 'nullable|numeric',
|
||||||
|
|
||||||
'items.*.cbm' => 'nullable|numeric',
|
'items.*.cbm' => 'nullable|numeric',
|
||||||
|
|
||||||
'items.*.kg' => 'nullable|numeric',
|
'items.*.kg' => 'nullable|numeric',
|
||||||
|
|
||||||
'items.*.shop_no' => 'nullable|string',
|
'items.*.shop_no' => 'nullable|string',
|
||||||
])['items'];
|
])['items'];
|
||||||
|
|
||||||
// रिकामे rows काढा
|
|
||||||
$items = array_filter($items, function ($row) {
|
$items = array_filter($items, function ($row) {
|
||||||
return trim($row['description'] ?? '') !== '';
|
return trim($row['description'] ?? '') !== '';
|
||||||
});
|
});
|
||||||
@@ -529,25 +572,20 @@ class AdminOrderController extends Controller
|
|||||||
return back()->with('error', 'Add at least one item.');
|
return back()->with('error', 'Add at least one item.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ BACKEND CALCULATION (DO NOT TRUST FRONTEND)
|
|
||||||
foreach ($items as &$item) {
|
foreach ($items as &$item) {
|
||||||
|
|
||||||
$ctn = (float) ($item['ctn'] ?? 0);
|
$ctn = (float) ($item['ctn'] ?? 0);
|
||||||
$qty = (float) ($item['qty'] ?? 0);
|
$qty = (float) ($item['qty'] ?? 0);
|
||||||
$price = (float) ($item['price'] ?? 0);
|
$price = (float) ($item['price'] ?? 0);
|
||||||
$cbm = (float) ($item['cbm'] ?? 0);
|
$cbm = (float) ($item['cbm'] ?? 0);
|
||||||
$kg = (float) ($item['kg'] ?? 0);
|
$kg = (float) ($item['kg'] ?? 0);
|
||||||
|
|
||||||
// Calculated fields
|
|
||||||
$item['ttl_qty'] = $ctn * $qty;
|
$item['ttl_qty'] = $ctn * $qty;
|
||||||
$item['ttl_amount'] = $item['ttl_qty'] * $price;
|
$item['ttl_amount'] = $item['ttl_qty'] * $price;
|
||||||
$item['ttl_cbm'] = $cbm * $ctn;
|
$item['ttl_cbm'] = $cbm * $ctn;
|
||||||
$item['ttl_kg'] = $ctn * $kg;
|
$item['ttl_kg'] = $ctn * $kg;
|
||||||
}
|
}
|
||||||
unset($item); // VERY IMPORTANT
|
unset($item);
|
||||||
|
|
||||||
|
|
||||||
// 3) totals
|
|
||||||
$total_ctn = array_sum(array_column($items, 'ctn'));
|
$total_ctn = array_sum(array_column($items, 'ctn'));
|
||||||
$total_qty = array_sum(array_column($items, 'qty'));
|
$total_qty = array_sum(array_column($items, 'qty'));
|
||||||
$total_ttl_qty = array_sum(array_column($items, 'ttl_qty'));
|
$total_ttl_qty = array_sum(array_column($items, 'ttl_qty'));
|
||||||
@@ -557,10 +595,8 @@ class AdminOrderController extends Controller
|
|||||||
$total_kg = array_sum(array_column($items, 'kg'));
|
$total_kg = array_sum(array_column($items, 'kg'));
|
||||||
$total_ttl_kg = array_sum(array_column($items, 'ttl_kg'));
|
$total_ttl_kg = array_sum(array_column($items, 'ttl_kg'));
|
||||||
|
|
||||||
// 4) order id generate
|
|
||||||
$orderId = $this->generateOrderId();
|
$orderId = $this->generateOrderId();
|
||||||
|
|
||||||
// 5) order create
|
|
||||||
$order = Order::create([
|
$order = Order::create([
|
||||||
'order_id' => $orderId,
|
'order_id' => $orderId,
|
||||||
'mark_no' => $request->mark_no,
|
'mark_no' => $request->mark_no,
|
||||||
@@ -577,7 +613,6 @@ class AdminOrderController extends Controller
|
|||||||
'status' => 'order_placed',
|
'status' => 'order_placed',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 6) order items
|
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
OrderItem::create([
|
OrderItem::create([
|
||||||
'order_id' => $order->id,
|
'order_id' => $order->id,
|
||||||
@@ -596,17 +631,14 @@ class AdminOrderController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7) invoice number
|
|
||||||
$invoiceNumber = $this->generateInvoiceNumber();
|
$invoiceNumber = $this->generateInvoiceNumber();
|
||||||
|
|
||||||
// 8) customer fetch
|
|
||||||
$markList = MarkList::where('mark_no', $order->mark_no)->first();
|
$markList = MarkList::where('mark_no', $order->mark_no)->first();
|
||||||
$customer = null;
|
$customer = null;
|
||||||
if ($markList && $markList->customer_id) {
|
if ($markList && $markList->customer_id) {
|
||||||
$customer = User::where('customer_id', $markList->customer_id)->first();
|
$customer = User::where('customer_id', $markList->customer_id)->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9) invoice create
|
|
||||||
$invoice = Invoice::create([
|
$invoice = Invoice::create([
|
||||||
'order_id' => $order->id,
|
'order_id' => $order->id,
|
||||||
'customer_id' => $customer->id ?? null,
|
'customer_id' => $customer->id ?? null,
|
||||||
@@ -631,7 +663,6 @@ class AdminOrderController extends Controller
|
|||||||
'pdf_path' => null,
|
'pdf_path' => null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 10) invoice items
|
|
||||||
foreach ($order->items as $item) {
|
foreach ($order->items as $item) {
|
||||||
InvoiceItem::create([
|
InvoiceItem::create([
|
||||||
'invoice_id' => $invoice->id,
|
'invoice_id' => $invoice->id,
|
||||||
@@ -673,7 +704,6 @@ class AdminOrderController extends Controller
|
|||||||
'shop_no' => 'nullable|string',
|
'shop_no' => 'nullable|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ✅ BACKEND CALCULATION
|
|
||||||
$ctn = (float) ($request->ctn ?? 0);
|
$ctn = (float) ($request->ctn ?? 0);
|
||||||
$qty = (float) ($request->qty ?? 0);
|
$qty = (float) ($request->qty ?? 0);
|
||||||
$price = (float) ($request->price ?? 0);
|
$price = (float) ($request->price ?? 0);
|
||||||
@@ -696,49 +726,10 @@ class AdminOrderController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$this->recalcTotals($order);
|
$this->recalcTotals($order);
|
||||||
$this->updateInvoiceFromOrder($order);
|
|
||||||
|
|
||||||
return back()->with('success', 'Item updated successfully');
|
return back()->with('success', 'Item updated successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private function updateInvoiceFromOrder(Order $order)
|
|
||||||
{
|
|
||||||
$invoice = Invoice::where('order_id', $order->id)->first();
|
|
||||||
|
|
||||||
if (!$invoice) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$invoice->final_amount = $order->ttl_amount;
|
|
||||||
$invoice->gst_percent = 0;
|
|
||||||
$invoice->gst_amount = 0;
|
|
||||||
$invoice->final_amount_with_gst = $order->ttl_amount;
|
|
||||||
$invoice->save();
|
|
||||||
|
|
||||||
InvoiceItem::where('invoice_id', $invoice->id)->delete();
|
|
||||||
|
|
||||||
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,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public function uploadExcelPreview(Request $request)
|
public function uploadExcelPreview(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -766,6 +757,4 @@ public function uploadExcelPreview(Request $request)
|
|||||||
], 500);
|
], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Order;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
@@ -12,45 +11,98 @@ class AdminReportController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display the reports page with joined data
|
* Display the reports page with joined data
|
||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
// public function index(Request $request)
|
||||||
{
|
// {
|
||||||
// -------------------------------
|
/*********************************************************
|
||||||
// FETCH REPORT DATA
|
* OLD FLOW (Order + Shipment + Invoice)
|
||||||
// ONLY orders that have BOTH:
|
* फक्त reference साठी ठेवलेला, वापरत नाही.
|
||||||
// 1. Invoice
|
*********************************************************/
|
||||||
// 2. Shipment
|
|
||||||
// -------------------------------
|
/*
|
||||||
$reports = DB::table('orders')
|
$reports = DB::table('orders')
|
||||||
->join('shipment_items', 'shipment_items.order_id', '=', 'orders.id')
|
->join('shipment_items', 'shipment_items.order_id', '=', 'orders.id')
|
||||||
->join('shipments', 'shipments.id', '=', 'shipment_items.shipment_id')
|
->join('shipments', 'shipments.id', '=', 'shipment_items.shipment_id')
|
||||||
->join('invoices', 'invoices.order_id', '=', 'orders.id')
|
->join('invoices', 'invoices.order_id', '=', 'orders.id')
|
||||||
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'orders.mark_no')
|
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'orders.mark_no')
|
||||||
->leftJoin('users', 'users.customer_id', '=', 'mark_list.customer_id')
|
->leftJoin('users', 'users.customer_id', '=', 'mark_list.customer_id')
|
||||||
|
->select(...)
|
||||||
|
->orderBy('shipments.shipment_date', 'desc')
|
||||||
|
->get();
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*********************************************************
|
||||||
|
* NEW FLOW (Container + Invoice + MarkList)
|
||||||
|
*********************************************************/
|
||||||
|
|
||||||
|
// $reports = DB::table('invoices')
|
||||||
|
// ->join('containers', 'containers.id', '=', 'invoices.containerid')
|
||||||
|
// ->leftJoin('mark_list', 'mark_list.markno', '=', 'invoices.markno')
|
||||||
|
// ->select(
|
||||||
|
// 'invoices.id as invoicepk',
|
||||||
|
// 'invoices.invoicenumber',
|
||||||
|
// 'invoices.invoicedate',
|
||||||
|
// 'invoices.finalamount',
|
||||||
|
// 'invoices.finalamountwithgst',
|
||||||
|
// 'invoices.gstpercent',
|
||||||
|
// 'invoices.gstamount',
|
||||||
|
// 'invoices.status as invoicestatus',
|
||||||
|
// 'invoices.markno',
|
||||||
|
|
||||||
|
// 'containers.id as containerpk',
|
||||||
|
// 'containers.containernumber',
|
||||||
|
// 'containers.containerdate',
|
||||||
|
// 'containers.containername',
|
||||||
|
|
||||||
|
// 'mark_list.companyname',
|
||||||
|
// 'mark_list.customername'
|
||||||
|
// )
|
||||||
|
// ->orderBy('containers.containerdate', 'desc')
|
||||||
|
// ->get();
|
||||||
|
|
||||||
|
// return view('admin.reports', compact('reports'));
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$reports = DB::table('invoices')
|
||||||
|
->join('containers', 'containers.id', '=', 'invoices.container_id')
|
||||||
|
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
|
||||||
->select(
|
->select(
|
||||||
'orders.id as order_pk',
|
// INVOICE
|
||||||
'orders.order_id',
|
'invoices.id as invoicepk',
|
||||||
'orders.mark_no',
|
|
||||||
'orders.origin',
|
|
||||||
'orders.destination',
|
|
||||||
|
|
||||||
'shipments.id as shipment_pk',
|
|
||||||
'shipments.shipment_id',
|
|
||||||
'shipments.status as shipment_status',
|
|
||||||
'shipments.shipment_date',
|
|
||||||
|
|
||||||
'invoices.invoice_number',
|
'invoices.invoice_number',
|
||||||
'invoices.invoice_date',
|
'invoices.invoice_date',
|
||||||
'invoices.final_amount',
|
'invoices.final_amount',
|
||||||
'invoices.status as invoice_status',
|
'invoices.final_amount_with_gst',
|
||||||
|
'invoices.gst_percent',
|
||||||
|
'invoices.gst_amount',
|
||||||
|
'invoices.status as invoicestatus',
|
||||||
|
'invoices.mark_no',
|
||||||
|
|
||||||
'mark_list.company_name',
|
// CONTAINER
|
||||||
'mark_list.customer_name'
|
'containers.id as containerpk',
|
||||||
|
'containers.container_number',
|
||||||
|
'containers.container_date',
|
||||||
|
'containers.container_name',
|
||||||
|
|
||||||
|
// RAW FIELDS (for reference/debug if needed)
|
||||||
|
'invoices.company_name as inv_company_name',
|
||||||
|
'invoices.customer_name as inv_customer_name',
|
||||||
|
'mark_list.company_name as ml_company_name',
|
||||||
|
'mark_list.customer_name as ml_customer_name',
|
||||||
|
|
||||||
|
// FINAL FIELDS (automatically pick invoice first, else mark_list)
|
||||||
|
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
|
||||||
|
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name')
|
||||||
)
|
)
|
||||||
|
->orderBy('invoices.invoice_date', 'desc')
|
||||||
|
->orderBy('invoices.id', 'desc')
|
||||||
|
|
||||||
->orderBy('shipments.shipment_date', 'desc')
|
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
return view('admin.reports', compact('reports'));
|
return view('admin.reports', compact('reports'));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
755
app/Http/Controllers/ContainerController.php
Normal file
755
app/Http/Controllers/ContainerController.php
Normal file
@@ -0,0 +1,755 @@
|
|||||||
|
<?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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($cleanedRows)) {
|
||||||
|
return back()
|
||||||
|
->withErrors(['excel_file' => 'No valid item rows found in Excel.'])
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FORMULA CHECK – UPDATED WITH AMOUNT FIX + NUMBER SANITIZER
|
||||||
|
*/
|
||||||
|
|
||||||
|
$cleanNumber = function ($value) {
|
||||||
|
if (is_string($value)) {
|
||||||
|
$value = str_replace(',', '', trim($value));
|
||||||
|
}
|
||||||
|
return is_numeric($value) ? (float)$value : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
$formulaErrors = [];
|
||||||
|
|
||||||
|
foreach ($cleanedRows as $item) {
|
||||||
|
$row = $item['row'];
|
||||||
|
$offset = $item['offset'];
|
||||||
|
|
||||||
|
$ctn = $essentialColumns['ctn_col'] !== null ? $cleanNumber($row[$essentialColumns['ctn_col']] ?? 0) : 0;
|
||||||
|
$qty = $essentialColumns['qty_col'] !== null ? $cleanNumber($row[$essentialColumns['qty_col']] ?? 0) : 0;
|
||||||
|
$ttlQ = $essentialColumns['totalqty_col'] !== null ? $cleanNumber($row[$essentialColumns['totalqty_col']] ?? 0) : 0;
|
||||||
|
$cbm = $essentialColumns['cbm_col'] !== null ? $cleanNumber($row[$essentialColumns['cbm_col']] ?? 0) : 0;
|
||||||
|
$ttlC = $essentialColumns['totalcbm_col'] !== null ? $cleanNumber($row[$essentialColumns['totalcbm_col']] ?? 0) : 0;
|
||||||
|
$kg = $essentialColumns['kg_col'] !== null ? $cleanNumber($row[$essentialColumns['kg_col']] ?? 0) : 0;
|
||||||
|
$ttlK = $essentialColumns['totalkg_col'] !== null ? $cleanNumber($row[$essentialColumns['totalkg_col']] ?? 0) : 0;
|
||||||
|
|
||||||
|
$price = $essentialColumns['price_col'] !== null ? $cleanNumber($row[$essentialColumns['price_col']] ?? 0) : 0;
|
||||||
|
$ttlAmount = $essentialColumns['amount_col'] !== null ? $cleanNumber($row[$essentialColumns['amount_col']] ?? 0) : 0;
|
||||||
|
|
||||||
|
$desc = $essentialColumns['desc_col'] !== null ? (string)($row[$essentialColumns['desc_col']] ?? '') : '';
|
||||||
|
$mark = $essentialColumns['itemno_col'] !== null ? (string)($row[$essentialColumns['itemno_col']] ?? '') : '';
|
||||||
|
|
||||||
|
// expected
|
||||||
|
$expTtlQty = $qty * $ctn;
|
||||||
|
$expTtlCbm = $cbm * $ctn;
|
||||||
|
$expTtlKg = $kg * $ctn;
|
||||||
|
$expTtlAmount = ($qty * $ctn) * $price;
|
||||||
|
|
||||||
|
$rowErrors = [];
|
||||||
|
|
||||||
|
if (abs($ttlQ - $expTtlQty) > 0.01) {
|
||||||
|
$rowErrors['TOTAL QTY'] = [
|
||||||
|
'actual' => $ttlQ,
|
||||||
|
'expected' => $expTtlQty,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abs($ttlC - $expTtlCbm) > 0.0005) {
|
||||||
|
$rowErrors['TOTAL CBM'] = [
|
||||||
|
'actual' => $ttlC,
|
||||||
|
'expected' => $expTtlCbm,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abs($ttlK - $expTtlKg) > 0.01) {
|
||||||
|
$rowErrors['TOTAL KG'] = [
|
||||||
|
'actual' => $ttlK,
|
||||||
|
'expected' => $expTtlKg,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($essentialColumns['amount_col'] !== null && $essentialColumns['price_col'] !== null) {
|
||||||
|
if (abs($ttlAmount - $expTtlAmount) > 0.01) {
|
||||||
|
$rowErrors['TOTAL AMOUNT'] = [
|
||||||
|
'actual' => $ttlAmount,
|
||||||
|
'expected' => $expTtlAmount,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($rowErrors)) {
|
||||||
|
// full row data map for excel table
|
||||||
|
$rowData = [];
|
||||||
|
foreach ($header as $colIndex => $headingText) {
|
||||||
|
$value = $row[$colIndex] ?? null;
|
||||||
|
if (is_string($value)) $value = trim($value);
|
||||||
|
$rowData[$headingText] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$formulaErrors[] = [
|
||||||
|
'excel_row' => $headerRowIndex + 1 + $offset,
|
||||||
|
'mark_no' => $mark,
|
||||||
|
'description' => $desc,
|
||||||
|
'errors' => $rowErrors,
|
||||||
|
'data' => $rowData,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
|
||||||
|
$markErrors = [];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$markErrors[] = [
|
||||||
|
'excel_row' => $headerRowIndex + 1 + $offset,
|
||||||
|
'mark_no' => $rowMark,
|
||||||
|
'data' => $rowData,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($formulaErrors) || !empty($markErrors)) {
|
||||||
|
return back()
|
||||||
|
->withInput()
|
||||||
|
->with([
|
||||||
|
'formula_errors' => $formulaErrors,
|
||||||
|
'mark_errors' => $markErrors,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// इथे Mark No सेट करतो
|
||||||
|
$invoice->mark_no = $firstMark;
|
||||||
|
|
||||||
|
$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'];
|
||||||
|
$offset = $item['offset'];
|
||||||
|
|
||||||
|
$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;
|
||||||
|
|
||||||
|
$rowIndex = $headerRowIndex + 1 + $offset;
|
||||||
|
|
||||||
|
InvoiceItem::create([
|
||||||
|
'invoice_id' => $invoice->id,
|
||||||
|
'container_id' => $container->id,
|
||||||
|
'container_row_index' => $rowIndex,
|
||||||
|
'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;
|
||||||
|
|
||||||
|
// original update
|
||||||
|
$data = $row->data ?? [];
|
||||||
|
foreach ($cols as $colHeader => $value) {
|
||||||
|
$data[$colHeader] = $value;
|
||||||
|
}
|
||||||
|
$row->update([
|
||||||
|
'data' => $data,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// extra: update linked invoice items & invoice totals
|
||||||
|
$rowIndex = $row->row_index;
|
||||||
|
|
||||||
|
$ctn = (float) ($data['CTN'] ?? $data['CTNS'] ?? 0);
|
||||||
|
$qty = (float) ($data['QTY'] ?? 0);
|
||||||
|
$ttlQ = (float) ($data['TTLQTY'] ?? $data['TOTALQTY'] ?? $data['TTL/QTY'] ?? ($ctn * $qty));
|
||||||
|
$price = (float) ($data['PRICE'] ?? 0);
|
||||||
|
$cbm = (float) ($data['CBM'] ?? 0);
|
||||||
|
$ttlC = (float) ($data['TOTALCBM'] ?? $data['TTL CBM'] ?? ($cbm * $ctn));
|
||||||
|
$kg = (float) ($data['KG'] ?? 0);
|
||||||
|
$ttlK = (float) ($data['TOTALKG'] ?? $data['TTL KG'] ?? ($kg * $ctn));
|
||||||
|
$amount = (float) ($data['AMOUNT'] ?? ($price * $ttlQ));
|
||||||
|
$desc = $data['DESCRIPTION'] ?? $data['DESC'] ?? null;
|
||||||
|
|
||||||
|
$items = InvoiceItem::where('container_id', $container->id)
|
||||||
|
->where('container_row_index', $rowIndex)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$item->description = $desc;
|
||||||
|
$item->ctn = $ctn;
|
||||||
|
$item->qty = $qty;
|
||||||
|
$item->ttl_qty = $ttlQ;
|
||||||
|
$item->price = $price;
|
||||||
|
$item->ttl_amount = $amount;
|
||||||
|
$item->cbm = $cbm;
|
||||||
|
$item->ttl_cbm = $ttlC;
|
||||||
|
$item->kg = $kg;
|
||||||
|
$item->ttl_kg = $ttlK;
|
||||||
|
$item->save();
|
||||||
|
|
||||||
|
$invoice = $item->invoice;
|
||||||
|
if ($invoice) {
|
||||||
|
$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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('containers.show', $container->id)
|
||||||
|
->with('success', 'Excel rows updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// app/Http/Controllers/ContainerController.php
|
||||||
|
public function updateStatus(Request $request, Container $container)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'status' => 'required|in:pending,in-progress,completed,cancelled',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$container->status = $request->status;
|
||||||
|
$container->save();
|
||||||
|
|
||||||
|
// जर AJAX असेल तर JSON दे
|
||||||
|
if ($request->wantsJson() || $request->ajax()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $container->status,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// normal form submit असेल तर redirect
|
||||||
|
return back()->with('success', 'Container 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,41 +10,29 @@ class Invoice extends Model
|
|||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'order_id',
|
'container_id',
|
||||||
'customer_id',
|
'customer_id',
|
||||||
'mark_no',
|
'mark_no',
|
||||||
|
|
||||||
'invoice_number',
|
'invoice_number',
|
||||||
'invoice_date',
|
'invoice_date',
|
||||||
'due_date',
|
'due_date',
|
||||||
|
|
||||||
'payment_method',
|
'payment_method',
|
||||||
'reference_no',
|
'reference_no',
|
||||||
'status',
|
'status',
|
||||||
|
'final_amount',
|
||||||
'final_amount', // without tax
|
'gst_percent',
|
||||||
|
'gst_amount',
|
||||||
'tax_type', // gst / igst
|
|
||||||
'gst_percent', // only used for gst UI input
|
|
||||||
'cgst_percent',
|
|
||||||
'sgst_percent',
|
|
||||||
'igst_percent',
|
|
||||||
|
|
||||||
'gst_amount', // total tax amount
|
|
||||||
'final_amount_with_gst',
|
'final_amount_with_gst',
|
||||||
|
|
||||||
'customer_name',
|
'customer_name',
|
||||||
'company_name',
|
'company_name',
|
||||||
'customer_email',
|
'customer_email',
|
||||||
'customer_mobile',
|
'customer_mobile',
|
||||||
'customer_address',
|
'customer_address',
|
||||||
'pincode',
|
'pincode',
|
||||||
|
|
||||||
'pdf_path',
|
'pdf_path',
|
||||||
'notes',
|
'notes',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
/****************************
|
/****************************
|
||||||
* Relationships
|
* Relationships
|
||||||
****************************/
|
****************************/
|
||||||
@@ -54,9 +42,9 @@ class Invoice extends Model
|
|||||||
return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC');
|
return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function order()
|
public function container()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Order::class);
|
return $this->belongsTo(Container::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function customer()
|
public function customer()
|
||||||
@@ -64,11 +52,21 @@ class Invoice extends Model
|
|||||||
return $this->belongsTo(User::class, 'customer_id');
|
return $this->belongsTo(User::class, 'customer_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function installments()
|
||||||
|
{
|
||||||
|
return $this->hasMany(InvoiceInstallment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ SINGLE, correct relation
|
||||||
|
public function chargeGroups()
|
||||||
|
{
|
||||||
|
return $this->hasMany(\App\Models\InvoiceChargeGroup::class, 'invoice_id');
|
||||||
|
}
|
||||||
|
|
||||||
/****************************
|
/****************************
|
||||||
* Helper Functions
|
* Helper Functions
|
||||||
****************************/
|
****************************/
|
||||||
|
|
||||||
// Auto calculate GST fields (you can call this in controller before saving)
|
|
||||||
public function calculateTotals()
|
public function calculateTotals()
|
||||||
{
|
{
|
||||||
$gst = ($this->final_amount * $this->gst_percent) / 100;
|
$gst = ($this->final_amount * $this->gst_percent) / 100;
|
||||||
@@ -76,7 +74,6 @@ class Invoice extends Model
|
|||||||
$this->final_amount_with_gst = $this->final_amount + $gst;
|
$this->final_amount_with_gst = $this->final_amount + $gst;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check overdue status condition
|
|
||||||
public function isOverdue()
|
public function isOverdue()
|
||||||
{
|
{
|
||||||
return $this->status === 'pending' && now()->gt($this->due_date);
|
return $this->status === 'pending' && now()->gt($this->due_date);
|
||||||
@@ -84,27 +81,31 @@ class Invoice extends Model
|
|||||||
|
|
||||||
public function getShipment()
|
public function getShipment()
|
||||||
{
|
{
|
||||||
return $this->order?->shipments?->first();
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function installments()
|
// ✅ Charge groups total accessor
|
||||||
|
public function getChargeGroupsTotalAttribute()
|
||||||
{
|
{
|
||||||
return $this->hasMany(InvoiceInstallment::class);
|
// relation already loaded असेल तर collection वरून sum होईल
|
||||||
|
return (float) $this->chargeGroups->sum('total_charge');
|
||||||
}
|
}
|
||||||
|
|
||||||
// App\Models\Invoice.php
|
// ✅ Grand total accessor (items + GST + charge groups)
|
||||||
|
public function getGrandTotalWithChargesAttribute()
|
||||||
|
{
|
||||||
|
return (float) ($this->final_amount_with_gst ?? 0) + $this->charge_groups_total;
|
||||||
|
}
|
||||||
|
|
||||||
public function totalPaid()
|
public function totalPaid()
|
||||||
{
|
{
|
||||||
return $this->installments()->sum('amount');
|
return $this->installments->sum('amount');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function remainingAmount()
|
public function remainingAmount()
|
||||||
{
|
{
|
||||||
return max(
|
return $this->grand_total_with_charges - $this->totalPaid();
|
||||||
($this->final_amount_with_gst ?? 0) - $this->totalPaid(),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
27
app/Models/InvoiceChargeGroup.php
Normal file
27
app/Models/InvoiceChargeGroup.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class InvoiceChargeGroup extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'invoice_id',
|
||||||
|
'group_name',
|
||||||
|
'basis_type',
|
||||||
|
'basis_value',
|
||||||
|
'rate',
|
||||||
|
'total_charge',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function invoice()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Invoice::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function items()
|
||||||
|
{
|
||||||
|
return $this->hasMany(InvoiceChargeGroupItem::class, 'group_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/Models/InvoiceChargeGroupItem.php
Normal file
23
app/Models/InvoiceChargeGroupItem.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class InvoiceChargeGroupItem extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'group_id',
|
||||||
|
'invoice_item_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function group()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(InvoiceChargeGroup::class, 'group_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function item()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(InvoiceItem::class, 'invoice_item_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,4 +37,81 @@ class InvoiceItem extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Invoice::class);
|
return $this->belongsTo(Invoice::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function chargeGroupItems()
|
||||||
|
{
|
||||||
|
return $this->hasMany(InvoiceChargeGroupItem::class, 'invoice_item_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// हे helper: पहिला group fetch करून त्यावरून rate/total काढणे
|
||||||
|
public function getChargeRateAttribute()
|
||||||
|
{
|
||||||
|
$pivot = $this->chargeGroupItems->first();
|
||||||
|
if (!$pivot || !$pivot->group) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$group = $pivot->group;
|
||||||
|
|
||||||
|
// basis नुसार या item चा basis value
|
||||||
|
$basis = 0;
|
||||||
|
switch ($group->basis_type) {
|
||||||
|
case 'ttl_qty':
|
||||||
|
$basis = $this->ttl_qty;
|
||||||
|
break;
|
||||||
|
case 'amount':
|
||||||
|
$basis = $this->ttl_amount;
|
||||||
|
break;
|
||||||
|
case 'ttl_cbm':
|
||||||
|
$basis = $this->ttl_cbm;
|
||||||
|
break;
|
||||||
|
case 'ttl_kg':
|
||||||
|
$basis = $this->ttl_kg;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($basis <= 0 || ($group->basis_value ?? 0) <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// group चा rate field आधीच आहे, ते direct वापरू
|
||||||
|
return (float) $group->rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChargeTotalAttribute()
|
||||||
|
{
|
||||||
|
$pivot = $this->chargeGroupItems->first();
|
||||||
|
if (!$pivot || !$pivot->group) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$group = $pivot->group;
|
||||||
|
|
||||||
|
$basis = 0;
|
||||||
|
switch ($group->basis_type) {
|
||||||
|
case 'ttl_qty':
|
||||||
|
$basis = $this->ttl_qty;
|
||||||
|
break;
|
||||||
|
case 'amount':
|
||||||
|
$basis = $this->ttl_amount;
|
||||||
|
break;
|
||||||
|
case 'ttl_cbm':
|
||||||
|
$basis = $this->ttl_cbm;
|
||||||
|
break;
|
||||||
|
case 'ttl_kg':
|
||||||
|
$basis = $this->ttl_kg;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($basis <= 0 || ($group->basis_value ?? 0) <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// per unit rate
|
||||||
|
$rate = (float) $group->rate;
|
||||||
|
// item total = basis * rate
|
||||||
|
return $basis * $rate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,10 +58,10 @@ class Order extends Model
|
|||||||
return $this->belongsToMany(\App\Models\Shipment::class, 'shipment_items', 'order_id', 'shipment_id');
|
return $this->belongsToMany(\App\Models\Shipment::class, 'shipment_items', 'order_id', 'shipment_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function invoice()
|
// public function invoice()
|
||||||
{
|
// {
|
||||||
return $this->hasOne(\App\Models\Invoice::class, 'order_id', 'id');
|
// return $this->hasOne(\App\Models\Invoice::class, 'order_id', 'id');
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
const STATUS_LABELS = [
|
const STATUS_LABELS = [
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ class CreateInvoiceItemsTable extends Migration
|
|||||||
|
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unsignedBigInteger('container_id')->nullable()->after('invoice_id');
|
||||||
|
$table->integer('container_row_index')->nullable()->after('container_id');
|
||||||
|
|
||||||
// FK
|
// FK
|
||||||
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');
|
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');
|
||||||
});
|
});
|
||||||
@@ -49,4 +52,6 @@ class CreateInvoiceItemsTable extends Migration
|
|||||||
});
|
});
|
||||||
Schema::dropIfExists('invoice_items');
|
Schema::dropIfExists('invoice_items');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('invoice_charge_groups', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('invoice_id');
|
||||||
|
$table->string('group_name')->nullable(); // उदा. "FREIGHT", "HANDLING"
|
||||||
|
$table->enum('basis_type', ['ttl_qty', 'amount', 'ttl_cbm', 'ttl_kg']);
|
||||||
|
$table->decimal('basis_value', 15, 3)->default(0); // auto calculate केलेला total basis
|
||||||
|
$table->decimal('rate', 15, 3)->default(0); // per basis rate (helper)
|
||||||
|
$table->decimal('total_charge', 15, 2); // admin नी manually टाकलेला total
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreign('invoice_id')
|
||||||
|
->references('id')->on('invoices')
|
||||||
|
->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('invoice_charge_groups');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('invoice_charge_group_items', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('group_id');
|
||||||
|
$table->unsignedBigInteger('invoice_item_id');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreign('group_id')
|
||||||
|
->references('id')->on('invoice_charge_groups')
|
||||||
|
->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->foreign('invoice_item_id')
|
||||||
|
->references('id')->on('invoice_items')
|
||||||
|
->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('invoice_charge_group_items');
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('invoice_items', function (Blueprint $table) {
|
||||||
|
$table->unsignedBigInteger('container_id')->nullable()->after('invoice_id');
|
||||||
|
$table->integer('container_row_index')->nullable()->after('container_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('invoice_items', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['container_id', 'container_row_index']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
<style>
|
<style>
|
||||||
/* ---------- Base ---------- */
|
/* ---------- Base ---------- */
|
||||||
|
|
||||||
:root{
|
:root{
|
||||||
--primary-1:#1a2951;
|
--primary-1:#1a2951;
|
||||||
--primary-2:#243a72;
|
--primary-2:#243a72;
|
||||||
@@ -37,7 +38,7 @@ body {
|
|||||||
/* header */
|
/* header */
|
||||||
.account-header {
|
.account-header {
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
background: linear-gradient(90deg,var(--primary-1),var(--primary-2));
|
background:linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
padding: 22px 26px;
|
padding: 22px 26px;
|
||||||
border-radius: var(--rounded);
|
border-radius: var(--rounded);
|
||||||
box-shadow: 0 6px 18px rgba(34,50,90,0.12);
|
box-shadow: 0 6px 18px rgba(34,50,90,0.12);
|
||||||
@@ -62,7 +63,7 @@ body {
|
|||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
display:inline-flex; align-items:center; justify-content:center; gap:8px;
|
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;
|
color:#fff; border:none; padding:10px 16px; border-radius:10px; font-weight:600;
|
||||||
cursor:pointer; transition: transform .15s ease, box-shadow .15s;
|
cursor:pointer; transition: transform .15s ease, box-shadow .15s;
|
||||||
}
|
}
|
||||||
@@ -334,7 +335,7 @@ tr:hover td{ background:#fbfdff; }
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
/* margin-right:-550px; */
|
margin-right:-550px;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,7 +343,7 @@ tr:hover td{ background:#fbfdff; }
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
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%);
|
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 {
|
.modal-fade1 {
|
||||||
position:fixed;
|
position:fixed;
|
||||||
inset:0;
|
inset:0;
|
||||||
@@ -484,12 +485,15 @@ tr:hover td{ background:#fbfdff; }
|
|||||||
#entryOrdersModal {
|
#entryOrdersModal {
|
||||||
z-index: 1300;
|
z-index: 1300;
|
||||||
}
|
}
|
||||||
|
#entryOrdersModal {
|
||||||
|
z-index: 1300;
|
||||||
|
}
|
||||||
|
|
||||||
/* entry summary cards */
|
/* entry summary cards */
|
||||||
.entry-summary-cards {
|
.entry-summary-cards {
|
||||||
display:flex;
|
display:flex;
|
||||||
gap:16px;
|
gap:16px;
|
||||||
margin-bottom:20px;
|
margin-top:25px;
|
||||||
flex-wrap:wrap;
|
flex-wrap:wrap;
|
||||||
}
|
}
|
||||||
.entry-summary-card {
|
.entry-summary-card {
|
||||||
@@ -504,6 +508,184 @@ tr:hover td{ background:#fbfdff; }
|
|||||||
.entry-summary-label{ font-size:12px; color:var(--muted); }
|
.entry-summary-label{ font-size:12px; color:var(--muted); }
|
||||||
.entry-summary-value{ font-size:18px; font-weight:700; color:#253047; margin-top:6px; }
|
.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 */
|
/* installment modal */
|
||||||
#installmentModal .modal-box1 { max-width:720px; min-width:380px; }
|
#installmentModal .modal-box1 { max-width:720px; min-width:380px; }
|
||||||
|
|
||||||
@@ -828,7 +1010,10 @@ tr:hover td{ background:#fbfdff; }
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
background: linear-gradient(90deg, #f9fbff, #f7faff);
|
background:linear-gradient(90deg, #e6ebf5 0%, #f9fbff 100%);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid #eef3fb;
|
border: 1px solid #eef3fb;
|
||||||
}
|
}
|
||||||
@@ -1110,6 +1295,40 @@ tr:hover td{ background:#fbfdff; }
|
|||||||
.pagination-container {
|
.pagination-container {
|
||||||
margin-right: 0 !important;
|
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 */
|
/* Zoom out specific fix */
|
||||||
@@ -1146,6 +1365,10 @@ tr:hover td{ background:#fbfdff; }
|
|||||||
.pagination-info {
|
.pagination-info {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entry-summary-cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prevent horizontal scroll on very small screens */
|
/* Prevent horizontal scroll on very small screens */
|
||||||
@@ -1178,6 +1401,7 @@ html, body {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="account-container">
|
<div class="account-container">
|
||||||
@@ -1323,7 +1547,7 @@ html, body {
|
|||||||
<!-- CREATE ORDER POPUP MODAL -->
|
<!-- CREATE ORDER POPUP MODAL -->
|
||||||
<div class="create-order-modal" id="createOrderModal">
|
<div class="create-order-modal" id="createOrderModal">
|
||||||
<div class="modal-box">
|
<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>
|
<div style="font-size:20px; font-weight:800;">Create New Installment</div>
|
||||||
<button class="btn ghost" id="closeCreateModal" title="Close create form">✕</button>
|
<button class="btn ghost" id="closeCreateModal" title="Close create form">✕</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1415,37 +1639,64 @@ html, body {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ENTRY DETAILS MODAL -->
|
<!-- ENTRY DETAILS MODAL (Enhanced) -->
|
||||||
<div class="modal-fade1" id="entryDetailsModal">
|
<div class="modal-fade1 entry-details-modal" id="entryDetailsModal">
|
||||||
<div class="modal-box1 entry-details-modal">
|
<div class="modal-box1">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
<div class="entry-details-header" >
|
||||||
<div>
|
<h2 >Entry Details</h2>
|
||||||
<h2 style="margin:0;font-size:20px;color:#223256;font-weight:800">Entry Details — <span id="entryDetailsId">-</span></h2>
|
<div class="subtitle">
|
||||||
<div style="font-size:13px;color:var(--muted)">Complete view of all installments for this entry.</div>
|
<span id="entryDetailsId">Loading...</span>
|
||||||
|
<span>• Complete view of all installments for this entry</span>
|
||||||
</div>
|
</div>
|
||||||
<div><button class="btn ghost" onclick="closeEntryDetailsModal()">Close</button></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="entry-details-content">
|
||||||
<div class="entry-summary-cards">
|
<div class="entry-summary-cards">
|
||||||
<div class="entry-summary-card">
|
<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 class="entry-summary-value" id="originalAmount">-</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="entry-summary-card">
|
<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 class="entry-summary-value" id="totalProcessed">-</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="entry-summary-card">
|
<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 class="entry-summary-value" id="pendingBalance">-</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="entry-summary-card">
|
<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 class="entry-summary-value" id="totalInstallments">-</div>
|
||||||
</div>
|
</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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Installment</th>
|
<th>Installment</th>
|
||||||
@@ -1460,36 +1711,40 @@ html, body {
|
|||||||
<tr><td colspan="6" class="empty-state">No installments yet</td></tr>
|
<tr><td colspan="6" class="empty-state">No installments yet</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="display:flex; justify-content: flex-end; gap:12px; margin-top:16px;">
|
<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>
|
<button type="button" class="btn ghost" onclick="closeEntryDetailsModal()">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
@can('account.add_installment')
|
@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
|
@endcan
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ENTRY ORDERS MODAL -->
|
<!-- ENTRY ORDERS MODAL (Enhanced) -->
|
||||||
<div class="modal-fade1" id="entryOrdersModal">
|
<div class="modal-fade1 entry-orders-modal" id="entryOrdersModal">
|
||||||
<div class="modal-box1" style="max-width: 1000px;">
|
<div class="modal-box1">
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
|
<div class="entry-orders-header">
|
||||||
<div>
|
<h2>Entry Orders</h2>
|
||||||
<h2 style="margin:0;font-size:20px;color:#223256;font-weight:800;">
|
<div class="subtitle">
|
||||||
Entry Orders
|
<span id="entryOrdersEntryNo-span">Loading...</span>
|
||||||
<span id="entryOrdersEntryNo-span" style="font-size:14px;color:#6b7280;margin-left:8px;"></span>
|
<span>• All orders associated with this entry</span>
|
||||||
</h2>
|
|
||||||
<div style="font-size:13px;color:#6f7b8f;">
|
|
||||||
All orders associated with this entry.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn ghost" type="button" onclick="closeEntryOrdersModal()">
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div class="entry-orders-content">
|
||||||
<div class="orders-table-container">
|
<div class="orders-table-container">
|
||||||
<table class="orders-table" style="width:100%;border-collapse:collapse;font-size:13px;">
|
<table class="orders-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Order ID</th>
|
<th>Order ID</th>
|
||||||
@@ -1503,23 +1758,40 @@ html, body {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="entryOrdersTableBody">
|
<tbody id="entryOrdersTableBody">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7" class="empty-state">No orders associated with this entry</td>
|
<td colspan="7" class="empty-state">Loading orders...</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
</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 -->
|
<!-- Installment Modal -->
|
||||||
<div class="modal-fade1" id="installmentModal">
|
<div class="modal-fade1" id="installmentModal">
|
||||||
<div class="modal-box1" style="max-width:720px;">
|
<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>
|
<div style="font-size:18px;font-weight:800;color:#243a72;">+ Add New Installment</div>
|
||||||
<button class="btn ghost" onclick="closeInstallmentModal()">✕</button>
|
<button class="btn ghost" onclick="closeInstallmentModal()">✕</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -2785,8 +3057,15 @@ async function submitEditEntry(e) {
|
|||||||
|
|
||||||
|
|
||||||
function openEntryOrdersModal(entryNo) {
|
function openEntryOrdersModal(entryNo) {
|
||||||
|
// Set entry number in header
|
||||||
document.getElementById('entryOrdersEntryNo-span').textContent = `(${entryNo})`;
|
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');
|
const tbody = document.getElementById('entryOrdersTableBody');
|
||||||
tbody.innerHTML = `
|
tbody.innerHTML = `
|
||||||
<tr>
|
<tr>
|
||||||
@@ -2794,6 +3073,7 @@ async function submitEditEntry(e) {
|
|||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// API call: /admin/account/entry-orders/{entryno}
|
||||||
jsonFetch(`/admin/account/entry-orders/${encodeURIComponent(entryNo)}`, {
|
jsonFetch(`/admin/account/entry-orders/${encodeURIComponent(entryNo)}`, {
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
})
|
})
|
||||||
@@ -2819,29 +3099,44 @@ async function submitEditEntry(e) {
|
|||||||
|
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
let totalQuantity = 0;
|
||||||
|
let totalAmount = 0;
|
||||||
|
|
||||||
orders.forEach(order => {
|
orders.forEach(order => {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
|
|
||||||
const amountValue =
|
const idString = (order.orderid ?? order.id ?? '').toString().trim();
|
||||||
order.ttl_amount ??
|
const numericId = parseInt(idString, 10);
|
||||||
order.ttlamount ??
|
const formattedId = isNaN(numericId)
|
||||||
order.total_amount ??
|
? escapeHtml(idString)
|
||||||
order.order_amount ??
|
: 'KNT-25-' + String(numericId).padStart(8, '0');
|
||||||
order.amount ??
|
|
||||||
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 = `
|
tr.innerHTML = `
|
||||||
<td>${escapeHtml(order.order_id)}</td>
|
<td>
|
||||||
<td>${escapeHtml(order.mark_no ?? '')}</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.origin ?? '')}</td>
|
||||||
<td>${escapeHtml(order.destination ?? '')}</td>
|
<td>${escapeHtml(order.destination ?? '')}</td>
|
||||||
<td>${escapeHtml(order.ctn ?? '')}</td>
|
<td>${escapeHtml(order.ctn ?? '')}</td>
|
||||||
<td>${escapeHtml(order.qty ?? '')}</td>
|
<td><strong>${quantity}</strong></td>
|
||||||
<td>${formatCurrency(amountValue)}</td>
|
<td><strong>${formatCurrency(amountValue)}</strong></td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update summary
|
||||||
|
document.getElementById('ordersTotalCount').textContent = orders.length;
|
||||||
|
document.getElementById('ordersTotalQuantity').textContent = totalQuantity.toLocaleString();
|
||||||
|
document.getElementById('ordersTotalAmount').textContent = formatCurrency(totalAmount);
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
tbody.innerHTML = `
|
tbody.innerHTML = `
|
||||||
@@ -2854,7 +3149,6 @@ async function submitEditEntry(e) {
|
|||||||
document.getElementById('entryOrdersModal').classList.add('modal-open');
|
document.getElementById('entryOrdersModal').classList.add('modal-open');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function closeEntryOrdersModal() {
|
function closeEntryOrdersModal() {
|
||||||
document.getElementById('entryOrdersModal').classList.remove('modal-open');
|
document.getElementById('entryOrdersModal').classList.remove('modal-open');
|
||||||
}
|
}
|
||||||
@@ -2989,7 +3283,7 @@ function handleSearch(){
|
|||||||
updatePaginationControls();
|
updatePaginationControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Entry details & installments ---------- */
|
/* ---------- Entry details & installments (Enhanced) ---------- */
|
||||||
async function openEntryDetailsModal(entryNo) {
|
async function openEntryDetailsModal(entryNo) {
|
||||||
try {
|
try {
|
||||||
const res = await jsonFetch('/admin/account/entry/' + encodeURIComponent(entryNo));
|
const res = await jsonFetch('/admin/account/entry/' + encodeURIComponent(entryNo));
|
||||||
@@ -2999,58 +3293,109 @@ async function openEntryDetailsModal(entryNo) {
|
|||||||
const entry = res.entry;
|
const entry = res.entry;
|
||||||
currentEntry = entry;
|
currentEntry = entry;
|
||||||
|
|
||||||
|
// Set header info
|
||||||
document.getElementById('entryDetailsId').textContent = entry.entry_no;
|
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('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);
|
// Render installments table
|
||||||
document.getElementById('totalInstallments').textContent = entry.installments.length;
|
|
||||||
|
|
||||||
const tbody = document.getElementById('installmentsTableBody');
|
const tbody = document.getElementById('installmentsTableBody');
|
||||||
tbody.innerHTML = '';
|
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) => {
|
entry.installments.forEach((ins, idx) => {
|
||||||
const tr = document.createElement('tr');
|
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>
|
<td>
|
||||||
<select class="installment-status-dropdown"
|
<select class="installment-status-dropdown"
|
||||||
data-id="${ins.id}"
|
data-id="${ins.id}"
|
||||||
onchange="updateInstallmentStatus(${ins.id}, this.value)">
|
onchange="updateInstallmentStatus(${ins.id}, this.value)"
|
||||||
<option value="Pending" ${ins.status === 'Pending' ? 'selected' : ''}
|
title="Update installment status">
|
||||||
style="color: #f59e0b; font-weight: 500; padding: 10px;">
|
${statusOptionsHtml}
|
||||||
⏳ 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>
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show modal
|
||||||
document.getElementById('entryDetailsModal').classList.add('modal-open');
|
document.getElementById('entryDetailsModal').classList.add('modal-open');
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
802
resources/views/admin/container.blade.php
Normal file
802
resources/views/admin/container.blade.php
Normal file
@@ -0,0 +1,802 @@
|
|||||||
|
@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>
|
||||||
|
@can('containers.create')
|
||||||
|
<a href="{{ route('containers.create') }}" class="add-container-btn">
|
||||||
|
<i class="fas fa-plus-circle"></i>
|
||||||
|
Add New Container
|
||||||
|
</a>
|
||||||
|
@endcan
|
||||||
|
</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>
|
||||||
|
<span class="status-text">
|
||||||
|
{{ ucfirst(str_replace('-', ' ', $status)) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
|
||||||
|
@can('containers.view')
|
||||||
|
<a href="{{ route('containers.show', $container->id) }}" class="action-btn view-btn">
|
||||||
|
<i class="fas fa-eye"></i> View
|
||||||
|
</a>
|
||||||
|
@endcan
|
||||||
|
|
||||||
|
@can('containers.update_status')
|
||||||
|
<form action="{{ route('containers.update-status', $container->id) }}"
|
||||||
|
method="POST"
|
||||||
|
class="update-form ajax-status-form"
|
||||||
|
data-container-id="{{ $container->id }}">
|
||||||
|
@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>
|
||||||
|
</form>
|
||||||
|
@endcan
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@can('containers.delete')
|
||||||
|
<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>
|
||||||
|
@endcan
|
||||||
|
</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>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
document.querySelectorAll('.ajax-status-form .status-select').forEach(function (selectEl) {
|
||||||
|
selectEl.addEventListener('change', function (event) {
|
||||||
|
const form = event.target.closest('form');
|
||||||
|
const url = form.getAttribute('action');
|
||||||
|
const token = form.querySelector('input[name="_token"]').value;
|
||||||
|
const status = event.target.value;
|
||||||
|
|
||||||
|
console.log('Sending status update:', url, status);
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': token,
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ status: status })
|
||||||
|
})
|
||||||
|
.then(async res => {
|
||||||
|
console.log('Response status:', res.status);
|
||||||
|
let data = null;
|
||||||
|
try { data = await res.json(); } catch (e) {}
|
||||||
|
console.log('Response body:', data);
|
||||||
|
|
||||||
|
if (!res.ok || !data || !data.success) {
|
||||||
|
alert('Status update failed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 👉 UI update (badge text + color)
|
||||||
|
const item = form.closest('.container-item');
|
||||||
|
const badge = item.querySelector('.status-badge');
|
||||||
|
|
||||||
|
// text
|
||||||
|
// text
|
||||||
|
const pretty = data.status.replace('-', ' ');
|
||||||
|
const textEl = badge.querySelector('.status-text');
|
||||||
|
if (textEl) {
|
||||||
|
textEl.textContent =
|
||||||
|
pretty.charAt(0).toUpperCase() + pretty.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// classes
|
||||||
|
badge.classList.remove(
|
||||||
|
'status-pending',
|
||||||
|
'status-in-progress',
|
||||||
|
'status-completed',
|
||||||
|
'status-cancelled'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.status === 'pending') {
|
||||||
|
badge.classList.add('status-pending');
|
||||||
|
} else if (data.status === 'in-progress') {
|
||||||
|
badge.classList.add('status-in-progress');
|
||||||
|
} else if (data.status === 'completed') {
|
||||||
|
badge.classList.add('status-completed');
|
||||||
|
} else if (data.status === 'cancelled') {
|
||||||
|
badge.classList.add('status-cancelled');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error:', err);
|
||||||
|
alert('Network error while updating status');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@endsection
|
||||||
325
resources/views/admin/container_create.blade.php
Normal file
325
resources/views/admin/container_create.blade.php
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
@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; }
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #f5c2c7;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-row-box {
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-item {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-item span {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container-fluid cm-add-wrapper">
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="card cm-add-main-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Add Container</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
{{-- SUCCESS --}}
|
||||||
|
@if (session('success'))
|
||||||
|
<div class="alert alert-success">
|
||||||
|
{{ session('success') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- VALIDATION --}}
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<ul class="mb-0">
|
||||||
|
@foreach ($errors->all() as $error)
|
||||||
|
<li>{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- COMBINED ERROR PANEL (summary text) --}}
|
||||||
|
@if(session('formula_errors') || session('mark_errors'))
|
||||||
|
|
||||||
|
<div class="card error-card mb-3">
|
||||||
|
<div class="card-header bg-danger text-white small">
|
||||||
|
⚠ Excel Validation Issues Found
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="small mb-1">
|
||||||
|
Some rows in your Excel file have formula or mark issues.
|
||||||
|
See the table below, and detailed messages after the table.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 1) Excel-style table at top --}}
|
||||||
|
@php
|
||||||
|
$formulaErrors = session('formula_errors') ?? [];
|
||||||
|
$markErrors = session('mark_errors') ?? [];
|
||||||
|
|
||||||
|
$allRowsData = [];
|
||||||
|
|
||||||
|
foreach ($formulaErrors as $fe) {
|
||||||
|
if (!empty($fe['data'] ?? null)) {
|
||||||
|
$allRowsData[] = $fe['data'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($markErrors as $me) {
|
||||||
|
if (!empty($me['data'] ?? null)) {
|
||||||
|
$allRowsData[] = $me['data'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$headings = [];
|
||||||
|
if (!empty($allRowsData)) {
|
||||||
|
$headings = array_keys($allRowsData[0]);
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if(!empty($headings))
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header small">
|
||||||
|
Error rows in Excel view
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<div class="table-responsive" style="max-height:260px; overflow:auto;">
|
||||||
|
<table class="table table-sm table-bordered mb-0" style="font-size:11.5px;">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Excel Row</th>
|
||||||
|
<th>Mark No</th>
|
||||||
|
@foreach($headings as $head)
|
||||||
|
<th>{{ $head }}</th>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{-- Formula error rows --}}
|
||||||
|
@foreach($formulaErrors as $fe)
|
||||||
|
@php
|
||||||
|
$rowData = $fe['data'] ?? [];
|
||||||
|
@endphp
|
||||||
|
@if(!empty($rowData))
|
||||||
|
<tr class="table-danger">
|
||||||
|
<td>{{ $fe['excel_row'] }}</td>
|
||||||
|
<td>{{ $fe['mark_no'] }}</td>
|
||||||
|
@foreach($headings as $head)
|
||||||
|
<td>{{ $rowData[$head] ?? '' }}</td>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
{{-- Mark error rows --}}
|
||||||
|
@foreach($markErrors as $me)
|
||||||
|
@php
|
||||||
|
$rowData = $me['data'] ?? [];
|
||||||
|
@endphp
|
||||||
|
@if(!empty($rowData))
|
||||||
|
<tr class="table-warning">
|
||||||
|
<td>{{ $me['excel_row'] }}</td>
|
||||||
|
<td>{{ $me['mark_no'] }}</td>
|
||||||
|
@foreach($headings as $head)
|
||||||
|
<td>{{ $rowData[$head] ?? '' }}</td>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- 2) Detailed per-row error boxes BELOW the table --}}
|
||||||
|
<div class="card error-card mb-3">
|
||||||
|
<div class="card-header bg-light small">
|
||||||
|
Detailed error messages
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
{{-- Formula Errors (detailed) --}}
|
||||||
|
@if(session('formula_errors'))
|
||||||
|
@foreach(session('formula_errors') as $fe)
|
||||||
|
<div class="error-row-box">
|
||||||
|
<div class="error-title">
|
||||||
|
Row {{ $fe['excel_row'] }}
|
||||||
|
@if($fe['mark_no']) | Mark: {{ $fe['mark_no'] }} @endif
|
||||||
|
@if($fe['description']) | {{ $fe['description'] }} @endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach($fe['errors'] as $field => $detail)
|
||||||
|
<div class="error-item text-danger">
|
||||||
|
❌ <span>{{ $field }}</span> →
|
||||||
|
Expected: {{ number_format($detail['expected'],4) }}
|
||||||
|
| Got: {{ number_format($detail['actual'],4) }}
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Mark Errors (detailed) --}}
|
||||||
|
@if(session('mark_errors'))
|
||||||
|
@foreach(session('mark_errors') as $me)
|
||||||
|
<div class="error-row-box bg-warning bg-opacity-10 border-warning">
|
||||||
|
<div class="error-title">
|
||||||
|
Row {{ $me['excel_row'] }}
|
||||||
|
</div>
|
||||||
|
<div class="error-item text-warning">
|
||||||
|
❌ Mark <strong>{{ $me['mark_no'] }}</strong> not found in database
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- FORM --}}
|
||||||
|
@if (!session('formula_errors') && !session('mark_errors'))
|
||||||
|
<form action="{{ route('containers.store') }}" method="POST" enctype="multipart/form-data" class="mt-3">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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
|
||||||
413
resources/views/admin/container_show.blade.php
Normal file
413
resources/views/admin/container_show.blade.php
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
@extends('admin.layouts.app')
|
||||||
|
|
||||||
|
@section('page-title', 'Container Details')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
/* ── TOP HEADER CARD ── */
|
||||||
|
.cm-header-card {
|
||||||
|
background: linear-gradient(100deg, #4c6fff 0%, #8e54e9 100%);
|
||||||
|
border-radius: 14px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 6px 24px rgba(76,111,255,0.22);
|
||||||
|
margin-bottom: 18px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.cm-header-card .card-body {
|
||||||
|
padding: 14px 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.cm-header-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.3px;
|
||||||
|
}
|
||||||
|
.cm-header-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.88;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── MAIN CARD ── */
|
||||||
|
.cm-main-card {
|
||||||
|
border-radius: 14px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 4px 20px rgba(15,35,52,0.10);
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.cm-main-card .card-header {
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #edf0f5;
|
||||||
|
padding: 12px 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.cm-main-card .card-header h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a2340;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── INFO STRIP ── */
|
||||||
|
.cm-info-strip {
|
||||||
|
display: flex;
|
||||||
|
gap: 28px;
|
||||||
|
padding: 12px 20px 0 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.cm-info-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #8a93a6;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.cm-info-value {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a2340;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── FILTER BAR ── */
|
||||||
|
.cm-filter-bar {
|
||||||
|
padding: 12px 20px 0 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.cm-row-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8a93a6;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.cm-filter-input {
|
||||||
|
max-width: 240px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid #dde2ee;
|
||||||
|
padding: 6px 14px;
|
||||||
|
outline: none;
|
||||||
|
transition: border 0.2s;
|
||||||
|
}
|
||||||
|
.cm-filter-input:focus {
|
||||||
|
border-color: #4c6fff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(76,111,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── TABLE SCROLL OUTER ── */
|
||||||
|
.cm-table-scroll-outer {
|
||||||
|
margin: 10px 14px 0 14px;
|
||||||
|
border-radius: 14px 14px 0 0;
|
||||||
|
/* इथे overflow: hidden होते, ते visible केले */
|
||||||
|
overflow-x: auto; /* फक्त horizontal scroll हवा असेल तर */
|
||||||
|
overflow-y: visible;
|
||||||
|
border: 1.5px solid #b8920e;
|
||||||
|
border-bottom: none;
|
||||||
|
box-shadow: 0 -2px 10px rgba(184,146,14,0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── TABLE WRAPPER ── */
|
||||||
|
.cm-table-wrapper {
|
||||||
|
position: relative;
|
||||||
|
/* इथे max-height, overflow auto काढले, जे inner scroll देत होते */
|
||||||
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── TABLE STYLES ── */
|
||||||
|
.cm-table {
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 1100px;
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
font-family: 'DM Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sticky header – light header color */
|
||||||
|
.cm-table thead tr th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 3;
|
||||||
|
background: #fde4b3;
|
||||||
|
color: #0a0a09;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 11px 14px;
|
||||||
|
border-bottom: 2px solid #fde4b3;
|
||||||
|
border-right: 1px solid #fde4b3;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
text-shadow: 0 1px 2px rgba(0,0,0,0.18);
|
||||||
|
}
|
||||||
|
.cm-table thead tr th:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* # column – narrower */
|
||||||
|
.cm-table thead tr th:first-child,
|
||||||
|
.cm-table tbody tr td:first-child {
|
||||||
|
width: 46px;
|
||||||
|
min-width: 46px;
|
||||||
|
max-width: 46px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Body rows */
|
||||||
|
.cm-table tbody tr td {
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-bottom: 1px solid #f0f3fb;
|
||||||
|
border-right: 1px solid #f0f3fb;
|
||||||
|
color: #2d3a55;
|
||||||
|
font-size: 12.5px;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
background: #fff;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.cm-table tbody tr td:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zebra */
|
||||||
|
.cm-table tbody tr:nth-child(even) td {
|
||||||
|
background: #f8f9ff;
|
||||||
|
}
|
||||||
|
/* Hover */
|
||||||
|
.cm-table tbody tr:hover td {
|
||||||
|
background: #edf3ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Row number td */
|
||||||
|
.cm-row-num {
|
||||||
|
color: #8a93a6;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Editable cell input */
|
||||||
|
.cm-cell-input {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 90px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1.5px solid transparent;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-family: 'DM Sans', sans-serif;
|
||||||
|
color: #2d3a55;
|
||||||
|
padding: 3px 6px;
|
||||||
|
text-align: center;
|
||||||
|
transition: border 0.15s, background 0.15s, box-shadow 0.15s;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.cm-cell-input:hover {
|
||||||
|
border-color: #c9d4f5;
|
||||||
|
background: #f5f8ff;
|
||||||
|
}
|
||||||
|
.cm-cell-input:focus {
|
||||||
|
border-color: #4c6fff;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(76,111,255,0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Save button */
|
||||||
|
.cm-save-btn {
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 7px 18px;
|
||||||
|
background: linear-gradient(90deg, #4c6fff, #8e54e9);
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 3px 10px rgba(76,111,255,0.22);
|
||||||
|
transition: opacity 0.2s, transform 0.15s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.cm-save-btn:hover {
|
||||||
|
opacity: 0.92;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.cm-empty {
|
||||||
|
padding: 30px 20px;
|
||||||
|
color: #8a93a6;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling – आता फक्त horizontal ला लागू होईल */
|
||||||
|
.cm-table-scroll-outer::-webkit-scrollbar { height: 6px; }
|
||||||
|
.cm-table-scroll-outer::-webkit-scrollbar-track { background: #f0f3fb; border-radius: 4px; }
|
||||||
|
.cm-table-scroll-outer::-webkit-scrollbar-thumb { background: #c5cce8; border-radius: 4px; }
|
||||||
|
.cm-table-scroll-outer::-webkit-scrollbar-thumb:hover { background: #8a99d0; }
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.cm-header-card .card-body { flex-direction: column; align-items: flex-start; }
|
||||||
|
.cm-info-strip { gap: 16px; }
|
||||||
|
.cm-table-scroll-outer { overflow-x: auto; }
|
||||||
|
.cm-table { min-width: 900px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container-fluid cm-wrapper">
|
||||||
|
|
||||||
|
{{-- TOP GRADIENT HEADER --}}
|
||||||
|
<div class="card cm-header-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div>
|
||||||
|
<h4 class="cm-header-title">
|
||||||
|
Container: {{ $container->container_number }}
|
||||||
|
</h4>
|
||||||
|
<div class="cm-header-sub">
|
||||||
|
Edit loading list directly — scroll horizontally & 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-main-card">
|
||||||
|
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Container Information</h5>
|
||||||
|
@if(!$container->rows->isEmpty())
|
||||||
|
<button type="submit"
|
||||||
|
form="cm-edit-rows-form"
|
||||||
|
class="cm-save-btn">
|
||||||
|
💾 Save Changes
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- INFO STRIP --}}
|
||||||
|
<div class="cm-info-strip">
|
||||||
|
<div class="cm-info-item">
|
||||||
|
<div class="cm-info-label">Container</div>
|
||||||
|
<div class="cm-info-value">{{ $container->container_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="cm-info-item">
|
||||||
|
<div class="cm-info-label">Date</div>
|
||||||
|
<div class="cm-info-value">{{ $container->container_date?->format('d-m-Y') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="cm-info-item">
|
||||||
|
<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" style="color:#4c6fff;text-decoration:none;font-weight:600;">
|
||||||
|
📄 Download / View
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="cm-info-value" style="color:#b0b8cc;">Not uploaded</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($container->rows->isEmpty())
|
||||||
|
<div class="cm-empty">No entries found for this container.</div>
|
||||||
|
@else
|
||||||
|
@php
|
||||||
|
$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">
|
||||||
|
<span class="cm-row-count">
|
||||||
|
Total rows: {{ $container->rows->count() }} • Edit cells then click "Save Changes"
|
||||||
|
</span>
|
||||||
|
<input type="text"
|
||||||
|
id="cmRowSearch"
|
||||||
|
class="cm-filter-input"
|
||||||
|
placeholder="🔍 Quick search..."
|
||||||
|
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-scroll-outer">
|
||||||
|
<div class="cm-table-wrapper">
|
||||||
|
<table class="cm-table" id="cmExcelTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
@foreach($allHeadings as $heading)
|
||||||
|
<th>{{ $heading }}</th>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($container->rows as $index => $row)
|
||||||
|
<tr>
|
||||||
|
<td class="cm-row-num">{{ $index + 1 }}</td>
|
||||||
|
@foreach($allHeadings as $heading)
|
||||||
|
@php $value = $row->data[$heading] ?? ''; @endphp
|
||||||
|
<td>
|
||||||
|
<input type="text"
|
||||||
|
class="cm-cell-input"
|
||||||
|
name="rows[{{ $row->id }}][{{ $heading }}]"
|
||||||
|
value="{{ $value }}">
|
||||||
|
</td>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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++) {
|
||||||
|
const cells = rows[i].getElementsByTagName('td');
|
||||||
|
let match = false;
|
||||||
|
for (let j = 0; j < cells.length; j++) {
|
||||||
|
const txt = cells[j].textContent || cells[j].innerText;
|
||||||
|
if (txt.toLowerCase().indexOf(filter) > -1) {
|
||||||
|
match = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows[i].style.display = match ? '' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 17px 17px 0 0;
|
border-radius: 17px 17px 0 0;
|
||||||
background: #fceeb8ff;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
min-height: 54px;
|
min-height: 54px;
|
||||||
padding: 15px 26px 10px 22px;
|
padding: 15px 26px 10px 22px;
|
||||||
border-bottom: 1.4px solid #e8e2cf;
|
border-bottom: 1.4px solid #e8e2cf;
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
.invoice-management-title {
|
.invoice-management-title {
|
||||||
font-size: 1.32rem;
|
font-size: 1.32rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: #2451af;
|
color: #ffffffff;
|
||||||
letter-spacing: .08em;
|
letter-spacing: .08em;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
|
|
||||||
/* Center all table content */
|
/* Center all table content */
|
||||||
.table thead tr {
|
.table thead tr {
|
||||||
background: #feebbe !important;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table thead th:first-child {
|
.table thead th:first-child {
|
||||||
@@ -237,7 +237,7 @@
|
|||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
border: none;
|
border: none;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #343535;
|
color: #ffffffff;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 20px 15px;
|
padding: 20px 15px;
|
||||||
@@ -259,24 +259,32 @@
|
|||||||
/* Soft blue background for ALL table rows */
|
/* Soft blue background for ALL table rows */
|
||||||
.table-striped tbody tr {
|
.table-striped tbody tr {
|
||||||
background: #f0f8ff !important;
|
background: #f0f8ff !important;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.15s ease;
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
||||||
|
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 {
|
.table-striped tbody tr:hover {
|
||||||
background: #e6f3ff !important;
|
background: #e6f3ff !important;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
||||||
transform: translateY(-1px);
|
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(odd),
|
||||||
.table-striped tbody tr:nth-of-type(even) {
|
.table-striped tbody tr:nth-of-type(even) {
|
||||||
background: #f0f8ff !important;
|
background: #f0f8ff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Center all table cells with proper spacing */
|
|
||||||
.table td {
|
.table td {
|
||||||
padding: 18px 15px;
|
padding: 18px 15px;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -291,7 +299,7 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* First and last cell rounded corners */
|
|
||||||
.table td:first-child {
|
.table td:first-child {
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -587,7 +595,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.date-separator {
|
.date-separator {
|
||||||
color: #64748b;
|
color: #000000ff;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
}
|
}
|
||||||
@@ -1091,31 +1099,48 @@
|
|||||||
<!-- TOOLS ROW - Search, Filter, Create Button -->
|
<!-- TOOLS ROW - Search, Filter, Create Button -->
|
||||||
<div class="invoice-tools-row">
|
<div class="invoice-tools-row">
|
||||||
<!-- Search Box with Button -->
|
<!-- Search Box with Button -->
|
||||||
|
<form method="GET" action="{{ route('admin.invoices.index') }}">
|
||||||
|
<div class="invoice-tools-row">
|
||||||
<div style="display:flex; align-items:center; flex:1; max-width:500px; min-width:380px;">
|
<div style="display:flex; align-items:center; flex:1; max-width:500px; min-width:380px;">
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<i class="bi bi-search"></i>
|
<i class="bi bi-search"></i>
|
||||||
<input type="text" id="invoiceSearch" placeholder="Search by invoice number, customer name...">
|
<input type="text"
|
||||||
|
id="invoiceSearch"
|
||||||
|
name="search"
|
||||||
|
value="{{ request('search') }}"
|
||||||
|
placeholder="Search by invoice number, customer name...">
|
||||||
</div>
|
</div>
|
||||||
|
<button type="submit" id="searchButton" class="search-btn">
|
||||||
|
Search
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filter Group -->
|
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<select class="filter-select" id="statusFilter">
|
<select class="filter-select" id="statusFilter" name="status">
|
||||||
<option value="">All Status</option>
|
<option value="all">All Status</option>
|
||||||
<option value="paid">Paid</option>
|
<option value="paid" {{ request('status')=='paid' ? 'selected' : '' }}>Paid</option>
|
||||||
<option value="pending">Pending</option>
|
<option value="pending" {{ request('status')=='pending' ? 'selected' : '' }}>Pending</option>
|
||||||
<option value="overdue">Overdue</option>
|
<option value="overdue" {{ request('status')=='overdue' ? 'selected' : '' }}>Overdue</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- Date Range Picker -->
|
|
||||||
<div class="date-range-container">
|
<div class="date-range-container">
|
||||||
<input type="date" class="date-input" id="startDate">
|
<input type="date"
|
||||||
|
class="date-input"
|
||||||
|
id="startDate"
|
||||||
|
name="start_date"
|
||||||
|
value="{{ request('start_date') }}">
|
||||||
<span class="date-separator">to</span>
|
<span class="date-separator">to</span>
|
||||||
<input type="date" class="date-input" id="endDate">
|
<input type="date"
|
||||||
|
class="date-input"
|
||||||
|
id="endDate"
|
||||||
|
name="end_date"
|
||||||
|
value="{{ request('end_date') }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Create Invoice Button -->
|
<!-- Create Invoice Button -->
|
||||||
<!-- <a href="{{ route('admin.invoices.create') }}" class="create-invoice-btn">
|
<!-- <a href="{{ route('admin.invoices.create') }}" class="create-invoice-btn">
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -264,13 +264,24 @@ header .bi-bell .badge {
|
|||||||
</a>
|
</a>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
{{-- Shipments --}}
|
<!-- {{-- Shipments --}}
|
||||||
@can('shipment.view')
|
@can('shipment.view')
|
||||||
<a href="{{ route('admin.shipments') }}" class="{{ request()->routeIs('admin.shipments') ? 'active' : '' }}">
|
<a href="{{ route('admin.shipments') }}" class="{{ request()->routeIs('admin.shipments') ? 'active' : '' }}">
|
||||||
<i class="bi bi-truck"></i> Shipments
|
<i class="bi bi-truck"></i> Shipments
|
||||||
</a>
|
</a>
|
||||||
|
@endcan -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{{-- Container – NEW MENU --}}
|
||||||
|
@can('containers.view')
|
||||||
|
<a href="{{ route('containers.index') }}" class="{{ request()->routeIs('containers.*') ? 'active' : '' }}">
|
||||||
|
<i class="fa-solid fa-box"></i> Container
|
||||||
|
</a>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{{-- Invoice --}}
|
{{-- Invoice --}}
|
||||||
@can('invoice.view')
|
@can('invoice.view')
|
||||||
<a href="{{ route('admin.invoices.index') }}" class="{{ request()->routeIs('admin.invoices.index') ? 'active' : '' }}">
|
<a href="{{ route('admin.invoices.index') }}" class="{{ request()->routeIs('admin.invoices.index') ? 'active' : '' }}">
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
@extends('admin.layouts.app')
|
@extends('admin.layouts.app')
|
||||||
|
|
||||||
@section('page-title', 'Orders')
|
@section('page-title', 'Invoices')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<style>
|
<style>
|
||||||
/* ===== GLOBAL RESPONSIVE STYLES ===== */
|
|
||||||
html, body {
|
html, body {
|
||||||
overflow-x: hidden !important;
|
overflow-x: hidden !important;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Use Inter globally */
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||||
:root {
|
:root {
|
||||||
--admin-font: 'Inter', sans-serif;
|
--admin-font: 'Inter', sans-serif;
|
||||||
@@ -26,7 +24,6 @@
|
|||||||
font-family: var(--admin-font);
|
font-family: var(--admin-font);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* VARIABLES AND BASE STYLES */
|
|
||||||
:root {
|
:root {
|
||||||
--primary-color: #3b82f6;
|
--primary-color: #3b82f6;
|
||||||
--primary-hover: #2563eb;
|
--primary-hover: #2563eb;
|
||||||
@@ -66,7 +63,6 @@
|
|||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HEADER STATS CARDS */
|
|
||||||
.stats-container {
|
.stats-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
@@ -107,7 +103,6 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== DOWNLOAD BUTTONS STYLES ===== */
|
|
||||||
.download-section {
|
.download-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -162,7 +157,6 @@
|
|||||||
transform: none !important;
|
transform: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TABLE STRUCTURE & WRAPPER FOR RESPONSIVENESS */
|
|
||||||
.table-wrapper {
|
.table-wrapper {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -174,14 +168,13 @@
|
|||||||
|
|
||||||
.orders-table {
|
.orders-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 1300px;
|
min-width: 1000px;
|
||||||
border-collapse: separate;
|
border-collapse: separate;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--text-dark);
|
color: var(--text-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* GRADIENT HEADER STYLES */
|
|
||||||
.orders-table thead {
|
.orders-table thead {
|
||||||
background: var(--primary-gradient);
|
background: var(--primary-gradient);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
@@ -232,7 +225,6 @@
|
|||||||
background: var(--bg-hover);
|
background: var(--bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== ENHANCED STATUS BADGES ===== */
|
|
||||||
.status-badge {
|
.status-badge {
|
||||||
font-size: 11px !important;
|
font-size: 11px !important;
|
||||||
font-weight: 600 !important;
|
font-weight: 600 !important;
|
||||||
@@ -244,7 +236,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
||||||
border: 2px solid transparent !important;
|
border: 2px solid transparent !important;
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -275,7 +266,6 @@
|
|||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Status icons */
|
|
||||||
.status-icon {
|
.status-icon {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -285,7 +275,6 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Invoice Status Badges */
|
|
||||||
.status-paid {
|
.status-paid {
|
||||||
background: var(--success-gradient) !important;
|
background: var(--success-gradient) !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
@@ -307,57 +296,6 @@
|
|||||||
width: 99px;
|
width: 99px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shipment Status Badges */
|
|
||||||
.ship-pending {
|
|
||||||
background: var(--warning-gradient) !important;
|
|
||||||
color: #d97706 !important;
|
|
||||||
border-color: #f59e0b !important;
|
|
||||||
width: 99px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ship-in_transit {
|
|
||||||
background: var(--info-gradient) !important;
|
|
||||||
color: #1e40af !important;
|
|
||||||
border-color: #3b82f6 !important;
|
|
||||||
width: 99px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ship-dispatched {
|
|
||||||
background: var(--purple-gradient) !important;
|
|
||||||
color: #7e22ce !important;
|
|
||||||
border-color: #8b5cf6 !important;
|
|
||||||
width: 99px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ship-delivered {
|
|
||||||
background: var(--success-gradient) !important;
|
|
||||||
color: #065f46 !important;
|
|
||||||
border-color: #10b981 !important;
|
|
||||||
width: 99px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ACTION BUTTON */
|
|
||||||
.action-btn {
|
|
||||||
color: var(--text-muted);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
font-size: 18px;
|
|
||||||
background: #f8fafc;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn:hover {
|
|
||||||
color: var(--primary-color);
|
|
||||||
transform: scale(1.1);
|
|
||||||
background: #e0f2fe;
|
|
||||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* EMPTY STATE */
|
|
||||||
.no-data {
|
.no-data {
|
||||||
padding: 60px 40px;
|
padding: 60px 40px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -378,39 +316,6 @@
|
|||||||
color: #d1d5db;
|
color: #d1d5db;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* RESPONSIVE ADJUSTMENTS */
|
|
||||||
@media(max-width: 768px) {
|
|
||||||
.orders-title {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.orders-container {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-container {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-section {
|
|
||||||
justify-content: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-btn {
|
|
||||||
flex: 1;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Enhanced Pagination Styles ---------- */
|
|
||||||
.pagination-container {
|
.pagination-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -502,7 +407,6 @@
|
|||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search and Filter Bar */
|
|
||||||
.filter-bar {
|
.filter-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
@@ -577,58 +481,41 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media(max-width: 768px) {
|
@media(max-width: 768px) {
|
||||||
.filter-bar {
|
.orders-title { font-size: 24px; }
|
||||||
flex-direction: column;
|
.orders-container { padding: 16px; }
|
||||||
align-items: stretch;
|
.stats-container { grid-template-columns: 1fr; }
|
||||||
}
|
.stat-card { padding: 16px; }
|
||||||
|
.stat-value { font-size: 24px; }
|
||||||
|
|
||||||
.search-box {
|
.download-section { justify-content: stretch; }
|
||||||
min-width: 100%;
|
.download-btn { flex: 1; justify-content: center; }
|
||||||
}
|
|
||||||
|
|
||||||
.filter-select,
|
.filter-bar { flex-direction: column; align-items: stretch; }
|
||||||
.date-input {
|
.search-box { min-width: 100%; }
|
||||||
width: 100%;
|
.filter-select, .date-input { width: 100%; }
|
||||||
}
|
.date-range { flex-direction: column; align-items: stretch; }
|
||||||
|
|
||||||
.date-range {
|
.pagination-container { flex-direction: column; gap: 16px; }
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-container {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="orders-container">
|
<div class="orders-container">
|
||||||
<div class="orders-title">
|
<div class="orders-title">
|
||||||
<i class="fas fa-box-open"></i> Orders Management
|
<i class="fas fa-file-invoice-dollar"></i> Invoices Management
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats Cards -->
|
{{-- Stats Cards based on invoices --}}
|
||||||
<div class="stats-container">
|
<div class="stats-container">
|
||||||
@php
|
@php
|
||||||
$totalOrders = $orders->count();
|
$totalInvoices = $invoices->count();
|
||||||
$paidInvoices = $orders->filter(function($order) {
|
$paidInvoices = $invoices->where('invoice_status', 'paid')->count();
|
||||||
$status = $order->invoice?->status ?? 'pending';
|
$pendingInvoices = $invoices->where('invoice_status', 'pending')->count();
|
||||||
return strtolower($status) === 'paid';
|
$overdueInvoices = $invoices->where('invoice_status', 'overdue')->count();
|
||||||
})->count();
|
|
||||||
$pendingInvoices = $orders->filter(function($order) {
|
|
||||||
$status = $order->invoice?->status ?? 'pending';
|
|
||||||
return strtolower($status) === 'pending';
|
|
||||||
})->count();
|
|
||||||
$overdueInvoices = $orders->filter(function($order) {
|
|
||||||
$status = $order->invoice?->status ?? 'pending';
|
|
||||||
return strtolower($status) === 'overdue';
|
|
||||||
})->count();
|
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="stat-card total">
|
<div class="stat-card total">
|
||||||
<div class="stat-value">{{ $totalOrders }}</div>
|
<div class="stat-value">{{ $totalInvoices }}</div>
|
||||||
<div class="stat-label">Total Orders</div>
|
<div class="stat-label">Total Invoices</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card paid">
|
<div class="stat-card paid">
|
||||||
<div class="stat-value">{{ $paidInvoices }}</div>
|
<div class="stat-value">{{ $paidInvoices }}</div>
|
||||||
@@ -644,23 +531,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Download Buttons -->
|
|
||||||
<div class="download-section">
|
<div class="download-section">
|
||||||
<button class="download-btn pdf" id="downloadPdf" {{ $orders->count() == 0 ? 'disabled' : '' }}>
|
<button class="download-btn pdf" id="downloadPdf" {{ $invoices->count() == 0 ? 'disabled' : '' }}>
|
||||||
<i class="fas fa-file-pdf"></i>
|
<i class="fas fa-file-pdf"></i>
|
||||||
Download PDF
|
Download PDF
|
||||||
</button>
|
</button>
|
||||||
<button class="download-btn excel" id="downloadExcel" {{ $orders->count() == 0 ? 'disabled' : '' }}>
|
<button class="download-btn excel" id="downloadExcel" {{ $invoices->count() == 0 ? 'disabled' : '' }}>
|
||||||
<i class="fas fa-file-excel"></i>
|
<i class="fas fa-file-excel"></i>
|
||||||
Download Excel
|
Download Excel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filter Bar -->
|
|
||||||
<div class="filter-bar">
|
<div class="filter-bar">
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<i class="fas fa-search search-icon"></i>
|
<i class="fas fa-search search-icon"></i>
|
||||||
<input type="text" class="search-input" placeholder="Search orders..." id="searchInput">
|
<input type="text" class="search-input" placeholder="Search invoices..." id="searchInput">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<select class="filter-select" id="statusFilter">
|
<select class="filter-select" id="statusFilter">
|
||||||
@@ -670,14 +555,6 @@
|
|||||||
<option value="overdue">Overdue</option>
|
<option value="overdue">Overdue</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select class="filter-select" id="shipmentFilter">
|
|
||||||
<option value="">All Shipments</option>
|
|
||||||
<option value="pending">Pending</option>
|
|
||||||
<option value="in_transit">In Transit</option>
|
|
||||||
<option value="dispatched">Dispatched</option>
|
|
||||||
<option value="delivered">Delivered</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div class="date-range">
|
<div class="date-range">
|
||||||
<span class="date-label">From:</span>
|
<span class="date-label">From:</span>
|
||||||
<input type="date" class="date-input" id="fromDate">
|
<input type="date" class="date-input" id="fromDate">
|
||||||
@@ -686,100 +563,61 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(isset($orders) && $orders->count() > 0)
|
@if($invoices->count() > 0)
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="orders-table">
|
<table class="orders-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Order ID</th>
|
|
||||||
<th>Shipment ID</th>
|
|
||||||
<th>Customer ID</th>
|
|
||||||
<th>Company</th>
|
|
||||||
<th>Origin</th>
|
|
||||||
<th>Destination</th>
|
|
||||||
<th>Order Date</th>
|
|
||||||
<th>Invoice No</th>
|
<th>Invoice No</th>
|
||||||
<th>Invoice Date</th>
|
<th>Invoice Date</th>
|
||||||
|
<th>Mark No</th>
|
||||||
|
<th>Container No</th>
|
||||||
|
<th>Container Date</th>
|
||||||
|
<th>Company</th>
|
||||||
|
<th>Customer</th>
|
||||||
<th>Amount</th>
|
<th>Amount</th>
|
||||||
<th>Amount + GST</th>
|
<th>Amount + GST</th>
|
||||||
<th>Invoice Status</th>
|
<th>Status</th>
|
||||||
<th>Shipment Status</th>
|
|
||||||
<th>Action</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody id="ordersTableBody">
|
<tbody id="ordersTableBody">
|
||||||
@foreach($orders as $order)
|
@foreach($invoices as $inv)
|
||||||
@php
|
@php
|
||||||
$mark = $order->markList ?? null;
|
$status = strtolower($inv->invoice_status ?? 'pending');
|
||||||
$invoice = $order->invoice ?? null;
|
|
||||||
$shipment = $order->shipments->first() ?? null;
|
|
||||||
|
|
||||||
// Normalized status values for consistent CSS classes
|
|
||||||
$invoiceStatus = strtolower($invoice?->status ?? 'pending');
|
|
||||||
$shipmentStatus = strtolower($shipment?->status ?? 'pending');
|
|
||||||
$shipmentStatusForCss = str_replace([' ', '-'], '_', $shipmentStatus);
|
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ $order->order_id ?? '-' }}</td>
|
<td>{{ $inv->invoice_number }}</td>
|
||||||
<td>{{ $shipment?->shipment_id ?? '-' }}</td>
|
<td>{{ $inv->invoice_date ? \Carbon\Carbon::parse($inv->invoice_date)->format('d-m-Y') : '-' }}</td>
|
||||||
<td>{{ $mark?->customer_id ?? '-' }}</td>
|
<td>{{ $inv->mark_no ?? '-' }}</td>
|
||||||
<td>{{ $mark?->company_name ?? '-' }}</td>
|
<td>{{ $inv->container_number ?? '-' }}</td>
|
||||||
<td>{{ $mark?->origin ?? $order->origin ?? '-' }}</td>
|
<td>{{ $inv->container_date ? \Carbon\Carbon::parse($inv->container_date)->format('d-m-Y') : '-' }}</td>
|
||||||
<td>{{ $mark?->destination ?? $order->destination ?? '-' }}</td>
|
<td>{{ $inv->company_name ?? '-' }}</td>
|
||||||
<td>{{ $order->created_at ? $order->created_at->format('d-m-Y') : '-' }}</td>
|
<td>{{ $inv->customer_name ?? '-' }}</td>
|
||||||
|
<td>{{ $inv->final_amount ? '₹'.number_format($inv->final_amount, 2) : '-' }}</td>
|
||||||
<td>{{ $invoice?->invoice_number ?? '-' }}</td>
|
<td>{{ $inv->final_amount_with_gst ? '₹'.number_format($inv->final_amount_with_gst, 2) : '-' }}</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
{{ $invoice?->invoice_date ? date('d-m-Y', strtotime($invoice->invoice_date)) : '-' }}
|
<span class="status-badge status-{{ $status }}">
|
||||||
</td>
|
{{ ucfirst($status) }}
|
||||||
|
|
||||||
<td>
|
|
||||||
{{ $invoice?->final_amount ? '₹'.number_format($invoice->final_amount, 2) : '-' }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
{{ $invoice?->final_amount_with_gst ? '₹'.number_format($invoice->final_amount_with_gst, 2) : '-' }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<span class="status-badge status-{{ $invoiceStatus }}">
|
|
||||||
{{ ucfirst($invoiceStatus) }}
|
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
|
||||||
<span class="status-badge ship-{{ $shipmentStatusForCss }}">
|
|
||||||
{{ ucfirst($shipmentStatus) }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="text-center">
|
|
||||||
<a href="{{ route('admin.orders.see', $order->id) }}" title="View Details">
|
|
||||||
<i class="fas fa-eye action-btn"></i>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div> {{-- End table-wrapper --}}
|
</div>
|
||||||
|
|
||||||
<!-- Pagination Controls -->
|
|
||||||
<div class="pagination-container">
|
<div class="pagination-container">
|
||||||
<div class="pagination-info" id="pageInfo">Showing 1 to {{ min(10, count($orders)) }} of {{ count($orders) }} entries</div>
|
<div class="pagination-info" id="pageInfo">
|
||||||
|
Showing 1 to {{ min(10, count($invoices)) }} of {{ count($invoices) }} entries
|
||||||
|
</div>
|
||||||
<div class="pagination-controls">
|
<div class="pagination-controls">
|
||||||
<button class="pagination-img-btn" id="prevPageBtn" title="Previous page" disabled>
|
<button class="pagination-img-btn" id="prevPageBtn" title="Previous page" disabled>
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M10 12L6 8L10 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M10 12L6 8L10 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="pagination-pages" id="paginationPages">
|
<div class="pagination-pages" id="paginationPages"></div>
|
||||||
<!-- Page numbers will be inserted here -->
|
<button class="pagination-img-btn" id="nextPageBtn" title="Next page" {{ count($invoices) > 10 ? '' : 'disabled' }}>
|
||||||
</div>
|
|
||||||
<button class="pagination-img-btn" id="nextPageBtn" title="Next page" {{ count($orders) > 10 ? '' : 'disabled' }}>
|
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M6 4L10 8L6 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M6 4L10 8L6 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -789,222 +627,111 @@
|
|||||||
@else
|
@else
|
||||||
<div class="no-data">
|
<div class="no-data">
|
||||||
<i class="fas fa-inbox"></i>
|
<i class="fas fa-inbox"></i>
|
||||||
<div>No orders found</div>
|
<div>No invoices found</div>
|
||||||
<p class="text-muted">There are currently no orders in the system.</p>
|
<p class="text-muted">There are currently no invoices in the system.</p>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Pagination state
|
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
const itemsPerPage = 10;
|
const itemsPerPage = 10;
|
||||||
let allOrders = {!! $orders->toJson() !!};
|
let allInvoices = {!! $invoices->toJson() !!};
|
||||||
let filteredOrders = [...allOrders];
|
let filteredInvoices = [...allInvoices];
|
||||||
|
|
||||||
// Status icon helper functions
|
|
||||||
function getInvoiceStatusIcon(status) {
|
function getInvoiceStatusIcon(status) {
|
||||||
switch(status) {
|
switch(status) {
|
||||||
case 'paid':
|
case 'paid': return '<i class="fas fa-check-circle status-icon"></i>';
|
||||||
return '<i class="fas fa-check-circle status-icon"></i>';
|
case 'pending': return '<i class="fas fa-clock status-icon"></i>';
|
||||||
case 'pending':
|
case 'overdue': return '<i class="fas fa-exclamation-triangle status-icon"></i>';
|
||||||
return '<i class="fas fa-clock status-icon"></i>';
|
default: return '';
|
||||||
case 'overdue':
|
|
||||||
return '<i class="fas fa-exclamation-triangle status-icon"></i>';
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getShipmentStatusIcon(status) {
|
|
||||||
switch(status) {
|
|
||||||
case 'pending':
|
|
||||||
return '<i class="fas fa-clock status-icon"></i>';
|
|
||||||
case 'in_transit':
|
|
||||||
return '<i class="fas fa-truck status-icon"></i>';
|
|
||||||
case 'dispatched':
|
|
||||||
return '<i class="fas fa-paper-plane status-icon"></i>';
|
|
||||||
case 'delivered':
|
|
||||||
return '<i class="fas fa-check-circle status-icon"></i>';
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date validation
|
|
||||||
function isValidDate(dateString) {
|
function isValidDate(dateString) {
|
||||||
if (!dateString) return false;
|
if (!dateString) return false;
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return date instanceof Date && !isNaN(date);
|
return date instanceof Date && !isNaN(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Date comparison helper - FIXED to properly handle order date
|
|
||||||
function isDateBetween(dateToCheck, fromDate, toDate) {
|
function isDateBetween(dateToCheck, fromDate, toDate) {
|
||||||
if (!dateToCheck) return true; // If no date to check, don't filter it out
|
if (!dateToCheck) return true;
|
||||||
|
|
||||||
// Parse the date to check (order date)
|
|
||||||
const checkDate = new Date(dateToCheck);
|
const checkDate = new Date(dateToCheck);
|
||||||
if (!isValidDate(checkDate)) return true;
|
if (!isValidDate(checkDate)) return true;
|
||||||
|
|
||||||
// Set time to beginning of day for comparison
|
|
||||||
checkDate.setHours(0,0,0,0);
|
checkDate.setHours(0,0,0,0);
|
||||||
|
|
||||||
// Check from date
|
|
||||||
if (fromDate && isValidDate(fromDate)) {
|
if (fromDate && isValidDate(fromDate)) {
|
||||||
const from = new Date(fromDate);
|
const from = new Date(fromDate);
|
||||||
from.setHours(0,0,0,0);
|
from.setHours(0,0,0,0);
|
||||||
if (checkDate < from) return false;
|
if (checkDate < from) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check to date
|
|
||||||
if (toDate && isValidDate(toDate)) {
|
if (toDate && isValidDate(toDate)) {
|
||||||
const to = new Date(toDate);
|
const to = new Date(toDate);
|
||||||
to.setHours(23,59,59,999);
|
to.setHours(23,59,59,999);
|
||||||
if (checkDate > to) return false;
|
if (checkDate > to) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to check if an order matches all filters
|
function invoiceMatchesFilters(inv, searchTerm, statusFilter, fromDate, toDate) {
|
||||||
function orderMatchesFilters(order, searchTerm, statusFilter, shipmentFilter, fromDate, toDate) {
|
|
||||||
// Search term matching across multiple fields
|
|
||||||
let matchesSearch = true;
|
let matchesSearch = true;
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
const searchFields = [
|
const term = searchTerm.toLowerCase();
|
||||||
order.order_id?.toString().toLowerCase(),
|
const fields = [
|
||||||
order.shipments?.[0]?.shipment_id?.toString().toLowerCase(),
|
inv.invoice_number?.toString().toLowerCase(),
|
||||||
order.mark_list?.customer_id?.toString().toLowerCase(),
|
inv.mark_no?.toString().toLowerCase(),
|
||||||
order.mark_list?.company_name?.toString().toLowerCase(),
|
inv.container_number?.toString().toLowerCase(),
|
||||||
order.invoice?.invoice_number?.toString().toLowerCase(),
|
inv.company_name?.toString().toLowerCase(),
|
||||||
order.mark_list?.origin?.toString().toLowerCase(),
|
inv.customer_name?.toString().toLowerCase()
|
||||||
order.mark_list?.destination?.toString().toLowerCase()
|
|
||||||
];
|
];
|
||||||
matchesSearch = searchFields.some(field => field && field.includes(searchTerm.toLowerCase()));
|
matchesSearch = fields.some(f => f && f.includes(term));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invoice status filter with safe access and normalization
|
const status = (inv.invoice_status || 'pending').toLowerCase();
|
||||||
const invoiceStatus = (order.invoice?.status || 'pending').toLowerCase();
|
const matchesStatus = !statusFilter || status === statusFilter;
|
||||||
const matchesStatus = !statusFilter || invoiceStatus === statusFilter;
|
|
||||||
|
|
||||||
// Shipment status filter with safe access and normalization
|
const invDate = inv.invoice_date;
|
||||||
const shipmentStatus = (order.shipments?.[0]?.status || 'pending').toLowerCase();
|
const matchesDate = isDateBetween(invDate, fromDate, toDate);
|
||||||
const matchesShipment = !shipmentFilter || shipmentStatus === shipmentFilter;
|
|
||||||
|
|
||||||
// Date range filter - using order date (created_at)
|
return matchesSearch && matchesStatus && matchesDate;
|
||||||
const orderDate = order.created_at;
|
|
||||||
const matchesDate = isDateBetween(orderDate, fromDate, toDate);
|
|
||||||
|
|
||||||
return matchesSearch && matchesStatus && matchesShipment && matchesDate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize pagination and filters
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Set today as default "to" date
|
|
||||||
renderTable();
|
renderTable();
|
||||||
updatePaginationControls();
|
updatePaginationControls();
|
||||||
|
|
||||||
// Pagination events
|
|
||||||
document.getElementById('prevPageBtn').addEventListener('click', goToPreviousPage);
|
document.getElementById('prevPageBtn').addEventListener('click', goToPreviousPage);
|
||||||
document.getElementById('nextPageBtn').addEventListener('click', goToNextPage);
|
document.getElementById('nextPageBtn').addEventListener('click', goToNextPage);
|
||||||
|
|
||||||
// Filter events
|
|
||||||
document.getElementById('searchInput').addEventListener('input', handleFilter);
|
document.getElementById('searchInput').addEventListener('input', handleFilter);
|
||||||
document.getElementById('statusFilter').addEventListener('change', handleFilter);
|
document.getElementById('statusFilter').addEventListener('change', handleFilter);
|
||||||
document.getElementById('shipmentFilter').addEventListener('change', handleFilter);
|
|
||||||
document.getElementById('fromDate').addEventListener('change', handleFilter);
|
document.getElementById('fromDate').addEventListener('change', handleFilter);
|
||||||
document.getElementById('toDate').addEventListener('change', handleFilter);
|
document.getElementById('toDate').addEventListener('change', handleFilter);
|
||||||
|
|
||||||
// Download buttons
|
|
||||||
document.getElementById('downloadPdf').addEventListener('click', downloadPdf);
|
document.getElementById('downloadPdf').addEventListener('click', downloadPdf);
|
||||||
document.getElementById('downloadExcel').addEventListener('click', downloadExcel);
|
document.getElementById('downloadExcel').addEventListener('click', downloadExcel);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Download Functions with ALL filter parameters
|
|
||||||
function downloadPdf() {
|
|
||||||
if (filteredOrders.length === 0) {
|
|
||||||
showNotification('No data available to download', 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showNotification('Preparing PDF download...', 'info');
|
|
||||||
|
|
||||||
// Get all current filters
|
|
||||||
const searchTerm = document.getElementById('searchInput').value;
|
|
||||||
const statusFilter = document.getElementById('statusFilter').value;
|
|
||||||
const shipmentFilter = document.getElementById('shipmentFilter').value;
|
|
||||||
const fromDate = document.getElementById('fromDate').value;
|
|
||||||
const toDate = document.getElementById('toDate').value;
|
|
||||||
|
|
||||||
// Create download URL with all filters - ALWAYS include all filters
|
|
||||||
let downloadUrl = "{{ route('admin.orders.download.pdf') }}";
|
|
||||||
let params = new URLSearchParams();
|
|
||||||
|
|
||||||
// Always append all parameters, even if empty
|
|
||||||
params.append('search', searchTerm || '');
|
|
||||||
params.append('status', statusFilter || '');
|
|
||||||
params.append('shipment', shipmentFilter || '');
|
|
||||||
params.append('from_date', fromDate || '');
|
|
||||||
params.append('to_date', toDate || '');
|
|
||||||
|
|
||||||
// Trigger download with all parameters
|
|
||||||
window.location.href = downloadUrl + '?' + params.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadExcel() {
|
|
||||||
if (filteredOrders.length === 0) {
|
|
||||||
showNotification('No data available to download', 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showNotification('Preparing Excel download...', 'info');
|
|
||||||
|
|
||||||
// Get all current filters
|
|
||||||
const searchTerm = document.getElementById('searchInput').value;
|
|
||||||
const statusFilter = document.getElementById('statusFilter').value;
|
|
||||||
const shipmentFilter = document.getElementById('shipmentFilter').value;
|
|
||||||
const fromDate = document.getElementById('fromDate').value;
|
|
||||||
const toDate = document.getElementById('toDate').value;
|
|
||||||
|
|
||||||
// Create download URL with all filters - ALWAYS include all filters
|
|
||||||
let downloadUrl = "{{ route('admin.orders.download.excel') }}";
|
|
||||||
let params = new URLSearchParams();
|
|
||||||
|
|
||||||
// Always append all parameters, even if empty
|
|
||||||
params.append('search', searchTerm || '');
|
|
||||||
params.append('status', statusFilter || '');
|
|
||||||
params.append('shipment', shipmentFilter || '');
|
|
||||||
params.append('from_date', fromDate || '');
|
|
||||||
params.append('to_date', toDate || '');
|
|
||||||
|
|
||||||
// Trigger download with all parameters
|
|
||||||
window.location.href = downloadUrl + '?' + params.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter functionality
|
|
||||||
function handleFilter() {
|
function handleFilter() {
|
||||||
const searchTerm = document.getElementById('searchInput').value;
|
const searchTerm = document.getElementById('searchInput').value;
|
||||||
const statusFilter = document.getElementById('statusFilter').value;
|
const statusFilter = document.getElementById('statusFilter').value;
|
||||||
const shipmentFilter = document.getElementById('shipmentFilter').value;
|
|
||||||
const fromDate = document.getElementById('fromDate').value;
|
const fromDate = document.getElementById('fromDate').value;
|
||||||
const toDate = document.getElementById('toDate').value;
|
const toDate = document.getElementById('toDate').value;
|
||||||
|
|
||||||
// Apply all filters
|
filteredInvoices = allInvoices.filter(inv =>
|
||||||
filteredOrders = allOrders.filter(order =>
|
invoiceMatchesFilters(inv, searchTerm, statusFilter, fromDate, toDate)
|
||||||
orderMatchesFilters(order, searchTerm, statusFilter, shipmentFilter, fromDate, toDate)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
renderTable();
|
renderTable();
|
||||||
updatePaginationControls();
|
updatePaginationControls();
|
||||||
|
|
||||||
// Update download buttons state
|
const hasResults = filteredInvoices.length > 0;
|
||||||
const hasResults = filteredOrders.length > 0;
|
|
||||||
document.getElementById('downloadPdf').disabled = !hasResults;
|
document.getElementById('downloadPdf').disabled = !hasResults;
|
||||||
document.getElementById('downloadExcel').disabled = !hasResults;
|
document.getElementById('downloadExcel').disabled = !hasResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pagination Functions
|
|
||||||
function goToPreviousPage() {
|
function goToPreviousPage() {
|
||||||
if (currentPage > 1) {
|
if (currentPage > 1) {
|
||||||
currentPage--;
|
currentPage--;
|
||||||
@@ -1014,7 +741,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function goToNextPage() {
|
function goToNextPage() {
|
||||||
const totalPages = Math.ceil(filteredOrders.length / itemsPerPage);
|
const totalPages = Math.ceil(filteredInvoices.length / itemsPerPage);
|
||||||
if (currentPage < totalPages) {
|
if (currentPage < totalPages) {
|
||||||
currentPage++;
|
currentPage++;
|
||||||
renderTable();
|
renderTable();
|
||||||
@@ -1023,7 +750,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updatePaginationControls() {
|
function updatePaginationControls() {
|
||||||
const totalPages = Math.ceil(filteredOrders.length / itemsPerPage);
|
const totalPages = Math.ceil(filteredInvoices.length / itemsPerPage);
|
||||||
const prevBtn = document.getElementById('prevPageBtn');
|
const prevBtn = document.getElementById('prevPageBtn');
|
||||||
const nextBtn = document.getElementById('nextPageBtn');
|
const nextBtn = document.getElementById('nextPageBtn');
|
||||||
const pageInfo = document.getElementById('pageInfo');
|
const pageInfo = document.getElementById('pageInfo');
|
||||||
@@ -1032,38 +759,22 @@
|
|||||||
prevBtn.disabled = currentPage === 1;
|
prevBtn.disabled = currentPage === 1;
|
||||||
nextBtn.disabled = currentPage === totalPages || totalPages === 0;
|
nextBtn.disabled = currentPage === totalPages || totalPages === 0;
|
||||||
|
|
||||||
// Update page info text
|
|
||||||
const startIndex = (currentPage - 1) * itemsPerPage + 1;
|
const startIndex = (currentPage - 1) * itemsPerPage + 1;
|
||||||
const endIndex = Math.min(currentPage * itemsPerPage, filteredOrders.length);
|
const endIndex = Math.min(currentPage * itemsPerPage, filteredInvoices.length);
|
||||||
pageInfo.textContent = `Showing ${startIndex} to ${endIndex} of ${filteredOrders.length} entries`;
|
pageInfo.textContent = `Showing ${startIndex} to ${endIndex} of ${filteredInvoices.length} entries`;
|
||||||
|
|
||||||
// Generate page numbers
|
|
||||||
paginationPages.innerHTML = '';
|
paginationPages.innerHTML = '';
|
||||||
|
|
||||||
if (totalPages <= 7) {
|
if (totalPages <= 7) {
|
||||||
// Show all pages
|
for (let i = 1; i <= totalPages; i++) addPageButton(i, paginationPages);
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
|
||||||
addPageButton(i, paginationPages);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Show first page, current page range, and last page
|
|
||||||
addPageButton(1, paginationPages);
|
addPageButton(1, paginationPages);
|
||||||
|
if (currentPage > 3) paginationPages.innerHTML += '<span class="pagination-ellipsis">...</span>';
|
||||||
if (currentPage > 3) {
|
|
||||||
paginationPages.innerHTML += '<span class="pagination-ellipsis">...</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = Math.max(2, currentPage - 1);
|
const start = Math.max(2, currentPage - 1);
|
||||||
const end = Math.min(totalPages - 1, currentPage + 1);
|
const end = Math.min(totalPages - 1, currentPage + 1);
|
||||||
|
for (let i = start; i <= end; i++) addPageButton(i, paginationPages);
|
||||||
|
|
||||||
for (let i = start; i <= end; i++) {
|
if (currentPage < totalPages - 2) paginationPages.innerHTML += '<span class="pagination-ellipsis">...</span>';
|
||||||
addPageButton(i, paginationPages);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPage < totalPages - 2) {
|
|
||||||
paginationPages.innerHTML += '<span class="pagination-ellipsis">...</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
addPageButton(totalPages, paginationPages);
|
addPageButton(totalPages, paginationPages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1071,9 +782,7 @@
|
|||||||
function addPageButton(pageNumber, container) {
|
function addPageButton(pageNumber, container) {
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.className = 'pagination-page-btn';
|
button.className = 'pagination-page-btn';
|
||||||
if (pageNumber === currentPage) {
|
if (pageNumber === currentPage) button.classList.add('active');
|
||||||
button.classList.add('active');
|
|
||||||
}
|
|
||||||
button.textContent = pageNumber;
|
button.textContent = pageNumber;
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
currentPage = pageNumber;
|
currentPage = pageNumber;
|
||||||
@@ -1083,68 +792,92 @@
|
|||||||
container.appendChild(button);
|
container.appendChild(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render Table Function
|
|
||||||
function renderTable() {
|
function renderTable() {
|
||||||
const tbody = document.getElementById('ordersTableBody');
|
const tbody = document.getElementById('ordersTableBody');
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
if (filteredOrders.length === 0) {
|
if (filteredInvoices.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="14" class="text-center py-4 text-muted">No orders found matching your criteria.</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="10" class="text-center py-4 text-muted">No invoices found matching your criteria.</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate pagination
|
|
||||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
const endIndex = startIndex + itemsPerPage;
|
const endIndex = startIndex + itemsPerPage;
|
||||||
const paginatedItems = filteredOrders.slice(startIndex, endIndex);
|
const paginated = filteredInvoices.slice(startIndex, endIndex);
|
||||||
|
|
||||||
paginatedItems.forEach(order => {
|
|
||||||
const mark = order.mark_list || null;
|
|
||||||
const invoice = order.invoice || null;
|
|
||||||
const shipment = order.shipments?.[0] || null;
|
|
||||||
|
|
||||||
// Normalized status values matching Blade logic
|
|
||||||
const invoiceStatus = (invoice?.status || 'pending').toLowerCase();
|
|
||||||
const shipmentStatus = (shipment?.status || 'pending').toLowerCase();
|
|
||||||
const shipmentStatusForCss = shipmentStatus.replace(/[ -]/g, '_');
|
|
||||||
|
|
||||||
|
paginated.forEach(inv => {
|
||||||
|
const status = (inv.invoice_status || 'pending').toLowerCase();
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td>${order.order_id || '-'}</td>
|
<td>${inv.invoice_number || '-'}</td>
|
||||||
<td>${shipment?.shipment_id || '-'}</td>
|
<td>${inv.invoice_date ? new Date(inv.invoice_date).toLocaleDateString('en-GB') : '-'}</td>
|
||||||
<td>${mark?.customer_id || '-'}</td>
|
<td>${inv.mark_no || '-'}</td>
|
||||||
<td>${mark?.company_name || '-'}</td>
|
<td>${inv.container_number || '-'}</td>
|
||||||
<td>${mark?.origin || order.origin || '-'}</td>
|
<td>${inv.container_date ? new Date(inv.container_date).toLocaleDateString('en-GB') : '-'}</td>
|
||||||
<td>${mark?.destination || order.destination || '-'}</td>
|
<td>${inv.company_name || '-'}</td>
|
||||||
<td>${order.created_at ? new Date(order.created_at).toLocaleDateString('en-GB') : '-'}</td>
|
<td>${inv.customer_name || '-'}</td>
|
||||||
<td>${invoice?.invoice_number || '-'}</td>
|
<td>${inv.final_amount ? '₹' + Number(inv.final_amount).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2}) : '-'}</td>
|
||||||
<td>${invoice?.invoice_date ? new Date(invoice.invoice_date).toLocaleDateString('en-GB') : '-'}</td>
|
<td>${inv.final_amount_with_gst ? '₹' + Number(inv.final_amount_with_gst).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2}) : '-'}</td>
|
||||||
<td>${invoice?.final_amount ? '₹' + Number(invoice.final_amount).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2}) : '-'}</td>
|
|
||||||
<td>${invoice?.final_amount_with_gst ? '₹' + Number(invoice.final_amount_with_gst).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2}) : '-'}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<span class="status-badge status-${invoiceStatus}">${getInvoiceStatusIcon(invoiceStatus)}${invoiceStatus.charAt(0).toUpperCase() + invoiceStatus.slice(1)}</span>
|
<span class="status-badge status-${status}">
|
||||||
</td>
|
${getInvoiceStatusIcon(status)}${status.charAt(0).toUpperCase() + status.slice(1)}
|
||||||
<td>
|
</span>
|
||||||
<span class="status-badge ship-${shipmentStatusForCss}">${getShipmentStatusIcon(shipmentStatusForCss)}${shipmentStatus.charAt(0).toUpperCase() + shipmentStatus.slice(1)}</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<a href="/admin/orders/${order.id}/see" title="View Details">
|
|
||||||
<i class="fas fa-eye action-btn"></i>
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notification function
|
function downloadPdf() {
|
||||||
function showNotification(message, type = 'info') {
|
if (filteredInvoices.length === 0) {
|
||||||
// Remove existing notification
|
showNotification('No data available to download', 'warning');
|
||||||
const existingNotification = document.querySelector('.download-notification');
|
return;
|
||||||
if (existingNotification) {
|
|
||||||
existingNotification.remove();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showNotification('Preparing PDF download...', 'info');
|
||||||
|
|
||||||
|
const searchTerm = document.getElementById('searchInput').value;
|
||||||
|
const statusFilter = document.getElementById('statusFilter').value;
|
||||||
|
const fromDate = document.getElementById('fromDate').value;
|
||||||
|
const toDate = document.getElementById('toDate').value;
|
||||||
|
|
||||||
|
let downloadUrl = "{{ route('admin.orders.download.pdf') }}";
|
||||||
|
let params = new URLSearchParams();
|
||||||
|
params.append('search', searchTerm || '');
|
||||||
|
params.append('status', statusFilter || '');
|
||||||
|
params.append('from_date', fromDate || '');
|
||||||
|
params.append('to_date', toDate || '');
|
||||||
|
|
||||||
|
window.location.href = downloadUrl + '?' + params.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadExcel() {
|
||||||
|
if (filteredInvoices.length === 0) {
|
||||||
|
showNotification('No data available to download', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification('Preparing Excel download...', 'info');
|
||||||
|
|
||||||
|
const searchTerm = document.getElementById('searchInput').value;
|
||||||
|
const statusFilter = document.getElementById('statusFilter').value;
|
||||||
|
const fromDate = document.getElementById('fromDate').value;
|
||||||
|
const toDate = document.getElementById('toDate').value;
|
||||||
|
|
||||||
|
let downloadUrl = "{{ route('admin.orders.download.excel') }}";
|
||||||
|
let params = new URLSearchParams();
|
||||||
|
params.append('search', searchTerm || '');
|
||||||
|
params.append('status', statusFilter || '');
|
||||||
|
params.append('from_date', fromDate || '');
|
||||||
|
params.append('to_date', toDate || '');
|
||||||
|
|
||||||
|
window.location.href = downloadUrl + '?' + params.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotification(message, type = 'info') {
|
||||||
|
const existing = document.querySelector('.download-notification');
|
||||||
|
if (existing) existing.remove();
|
||||||
|
|
||||||
const notification = document.createElement('div');
|
const notification = document.createElement('div');
|
||||||
notification.className = `download-notification alert alert-${type}`;
|
notification.className = `download-notification alert alert-${type}`;
|
||||||
notification.style.cssText = `
|
notification.style.cssText = `
|
||||||
@@ -1177,7 +910,6 @@
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add CSS for notifications
|
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
|
|||||||
32
resources/views/admin/pdf/invoices_excel.blade.php
Normal file
32
resources/views/admin/pdf/invoices_excel.blade.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Invoice No</th>
|
||||||
|
<th>Invoice Date</th>
|
||||||
|
<th>Mark No</th>
|
||||||
|
<th>Container No</th>
|
||||||
|
<th>Container Date</th>
|
||||||
|
<th>Company</th>
|
||||||
|
<th>Customer</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Amount + GST</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($invoices as $inv)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $inv->invoice_number }}</td>
|
||||||
|
<td>{{ $inv->invoice_date ? \Carbon\Carbon::parse($inv->invoice_date)->format('d-m-Y') : '' }}</td>
|
||||||
|
<td>{{ $inv->mark_no }}</td>
|
||||||
|
<td>{{ $inv->container_number }}</td>
|
||||||
|
<td>{{ $inv->container_date ? \Carbon\Carbon::parse($inv->container_date)->format('d-m-Y') : '' }}</td>
|
||||||
|
<td>{{ $inv->company_name }}</td>
|
||||||
|
<td>{{ $inv->customer_name }}</td>
|
||||||
|
<td>{{ $inv->final_amount }}</td>
|
||||||
|
<td>{{ $inv->final_amount_with_gst }}</td>
|
||||||
|
<td>{{ $inv->invoice_status }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
48
resources/views/admin/pdf/invoices_report.blade.php
Normal file
48
resources/views/admin/pdf/invoices_report.blade.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Invoices Report</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: DejaVu Sans, sans-serif; font-size: 12px; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||||
|
th, td { border: 1px solid #ccc; padding: 6px 8px; text-align: left; }
|
||||||
|
th { background: #f3f4f6; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h3>Invoices Report</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Invoice No</th>
|
||||||
|
<th>Invoice Date</th>
|
||||||
|
<th>Mark No</th>
|
||||||
|
<th>Container No</th>
|
||||||
|
<th>Container Date</th>
|
||||||
|
<th>Company</th>
|
||||||
|
<th>Customer</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Amount + GST</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($invoices as $inv)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $inv->invoice_number ?? '-' }}</td>
|
||||||
|
<td>{{ $inv->invoice_date ? \Carbon\Carbon::parse($inv->invoice_date)->format('d-m-Y') : '-' }}</td>
|
||||||
|
<td>{{ $inv->mark_no ?? '-' }}</td>
|
||||||
|
<td>{{ $inv->container_number ?? '-' }}</td>
|
||||||
|
<td>{{ $inv->container_date ? \Carbon\Carbon::parse($inv->container_date)->format('d-m-Y') : '-' }}</td>
|
||||||
|
<td>{{ $inv->company_name ?? '-' }}</td>
|
||||||
|
<td>{{ $inv->customer_name ?? '-' }}</td>
|
||||||
|
<td>{{ $inv->final_amount ? number_format($inv->final_amount, 2) : '0.00' }}</td>
|
||||||
|
<td>{{ $inv->final_amount_with_gst ? number_format($inv->final_amount_with_gst, 2) : '0.00' }}</td>
|
||||||
|
<td>{{ ucfirst($inv->invoice_status ?? 'pending') }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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); }
|
.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 {
|
.priority-badge {
|
||||||
display: inline-flex; align-items: center; font-size: 13.5px; padding: 6px 16px; border-radius: 12px; font-weight: 600;
|
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;
|
color: #fff; margin: 2px 0; transition: transform 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
.priority-badge:hover { transform: scale(1.08); }
|
.priority-badge:hover { transform: scale(1.08); }
|
||||||
@@ -30,8 +30,8 @@
|
|||||||
.priority-medium { background: linear-gradient(135deg, #ffe390, #f5b041); }
|
.priority-medium { background: linear-gradient(135deg, #ffe390, #f5b041); }
|
||||||
.priority-low { background: linear-gradient(135deg, #b8f0c2, #1d8660); }
|
.priority-low { background: linear-gradient(135deg, #b8f0c2, #1d8660); }
|
||||||
.custom-table thead th {
|
.custom-table thead th {
|
||||||
text-align: center; font-weight: 700; color: #000; padding: 14px; font-size: 17px; letter-spacing: 0.5px;
|
text-align: center; font-weight: 700; color: #ffffffff; padding: 14px; font-size: 17px; letter-spacing: 0.5px;
|
||||||
border-bottom: 2px solid #bfbfbf; background-color: #fde4b3;
|
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:first-child { border-top-left-radius: 12px; }
|
||||||
.custom-table thead tr:first-child th:last-child { border-top-right-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;
|
box-shadow: 0 0 0 2px #ffffff;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
.custom-table th,
|
||||||
|
.custom-table td {
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!-- <!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Laravel Test Page</title>
|
<title>Laravel Test Page</title>
|
||||||
@@ -33,4 +33,4 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html> -->
|
||||||
|
|||||||
184
routes/web.php
184
routes/web.php
@@ -12,9 +12,8 @@ use App\Http\Controllers\Admin\AdminAccountController;
|
|||||||
use App\Http\Controllers\Admin\AdminReportController;
|
use App\Http\Controllers\Admin\AdminReportController;
|
||||||
use App\Http\Controllers\Admin\AdminStaffController;
|
use App\Http\Controllers\Admin\AdminStaffController;
|
||||||
use App\Http\Controllers\Admin\AdminChatController;
|
use App\Http\Controllers\Admin\AdminChatController;
|
||||||
use Illuminate\Session\Middleware\StartSession;
|
use Illuminate\Support\Facades\Broadcast;
|
||||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
use App\Http\Controllers\ContainerController;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@@ -24,33 +23,56 @@ Route::get('/', function () {
|
|||||||
return view('welcome');
|
return view('welcome');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// BROADCASTING AUTH (FOR ADMIN SESSION)
|
||||||
|
// ==========================================
|
||||||
|
Broadcast::routes(['middleware' => ['web']]);
|
||||||
|
|
||||||
|
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(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (auth('admin')->check()) {
|
||||||
|
\Log::info('✅ Admin authenticated', ['id' => auth('admin')->id()]);
|
||||||
|
return Broadcast::auth($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// ADMIN LOGIN ROUTES
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// login routes (public)
|
Route::prefix('admin')->group(function () {
|
||||||
Route::prefix('admin')->middleware('web')->group(function () {
|
|
||||||
Route::get('/login', [AdminAuthController::class, 'showLoginForm'])->name('admin.login');
|
Route::get('/login', [AdminAuthController::class, 'showLoginForm'])->name('admin.login');
|
||||||
Route::post('/login', [AdminAuthController::class, 'login'])->name('admin.login.submit');
|
Route::post('/login', [AdminAuthController::class, 'login'])->name('admin.login.submit');
|
||||||
Route::post('/logout', [AdminAuthController::class, 'logout'])->name('admin.logout');
|
Route::post('/logout', [AdminAuthController::class, 'logout'])->name('admin.logout');
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
Route::get('/login', function () {
|
|
||||||
return redirect()->route('admin.login');
|
|
||||||
})->name('login');
|
|
||||||
|
|
||||||
|
|
||||||
|
Broadcast::routes(['middleware' => ['web']]);
|
||||||
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// PROTECTED ADMIN ROUTES (session protected)
|
// PROTECTED ADMIN ROUTES (session protected)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
Route::prefix('admin')
|
Route::prefix('admin')
|
||||||
->middleware(['web', 'auth:admin'])
|
->middleware('auth:admin')
|
||||||
->group(function () {
|
->group(function () {
|
||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
Route::get('/dashboard', [AdminOrderController::class, 'index'])
|
Route::get('/dashboard', [AdminOrderController::class, 'dashboard'])
|
||||||
->name('admin.dashboard');
|
->name('admin.dashboard');
|
||||||
|
|
||||||
Route::get('/reports', [AdminReportController::class, 'index'])->name('admin.reports');
|
Route::get('/reports', [AdminReportController::class, 'index'])->name('admin.reports');
|
||||||
@@ -67,6 +89,41 @@ Route::prefix('admin')
|
|||||||
Route::get('/profile', fn() => view('admin.profile'))
|
Route::get('/profile', fn() => view('admin.profile'))
|
||||||
->name('admin.profile');
|
->name('admin.profile');
|
||||||
|
|
||||||
|
Route::post(
|
||||||
|
'admin/orders/upload-excel-preview',
|
||||||
|
[\App\Http\Controllers\Admin\AdminOrderController::class, 'uploadExcelPreview']
|
||||||
|
)->name('admin.orders.upload.excel.preview');
|
||||||
|
|
||||||
|
|
||||||
|
//---------------------------
|
||||||
|
// CONTAINER ROUTES
|
||||||
|
//---------------------------
|
||||||
|
// Index + list
|
||||||
|
//---------------------------
|
||||||
|
// CONTAINER ROUTES
|
||||||
|
//---------------------------
|
||||||
|
Route::get('/containers', [ContainerController::class, 'index'])
|
||||||
|
->name('containers.index');
|
||||||
|
|
||||||
|
Route::get('/containers/create', [ContainerController::class, 'create'])
|
||||||
|
->name('containers.create');
|
||||||
|
|
||||||
|
Route::post('/containers', [ContainerController::class, 'store'])
|
||||||
|
->name('containers.store');
|
||||||
|
|
||||||
|
Route::get('/containers/{container}', [ContainerController::class, 'show'])
|
||||||
|
->name('containers.show');
|
||||||
|
|
||||||
|
Route::post('/containers/{container}/update-rows', [ContainerController::class, 'updateRows'])
|
||||||
|
->name('containers.rows.update');
|
||||||
|
|
||||||
|
Route::post('containers/{container}/status', [ContainerController::class, 'updateStatus'])
|
||||||
|
->name('containers.update-status');
|
||||||
|
|
||||||
|
Route::delete('/containers/{container}', [ContainerController::class, 'destroy'])
|
||||||
|
->name('containers.destroy');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// USER REQUESTS
|
// USER REQUESTS
|
||||||
@@ -94,7 +151,6 @@ Route::prefix('admin')
|
|||||||
)->name('admin.profile.reject');
|
)->name('admin.profile.reject');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// MARK LIST
|
// MARK LIST
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@@ -106,15 +162,17 @@ Route::prefix('admin')
|
|||||||
|
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// ORDERS
|
// ORDERS (UPDATED)
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
Route::get('/orders', [AdminOrderController::class, 'orderShow'])
|
// मुख्य Orders पेज (invoice + container listing)
|
||||||
|
Route::get('/orders', [AdminOrderController::class, 'index'])
|
||||||
->name('admin.orders');
|
->name('admin.orders');
|
||||||
|
|
||||||
|
// जुनं list route (असल्या वापरासाठी पण index ला point)
|
||||||
Route::get('/orders/list', [AdminOrderController::class, 'index'])
|
Route::get('/orders/list', [AdminOrderController::class, 'index'])
|
||||||
->name('admin.orders.index');
|
->name('admin.orders.index');
|
||||||
|
|
||||||
|
// Order show (old single order view)
|
||||||
Route::get('/orders/{id}', [AdminOrderController::class, 'show'])
|
Route::get('/orders/{id}', [AdminOrderController::class, 'show'])
|
||||||
->name('admin.orders.show');
|
->name('admin.orders.show');
|
||||||
|
|
||||||
@@ -133,39 +191,29 @@ Route::prefix('admin')
|
|||||||
Route::get('/orders/view/{id}', [AdminOrderController::class, 'popup'])
|
Route::get('/orders/view/{id}', [AdminOrderController::class, 'popup'])
|
||||||
->name('admin.orders.popup');
|
->name('admin.orders.popup');
|
||||||
|
|
||||||
Route::post('/admin/orders/temp/add', [AdminOrderController::class, 'addTempItem'])
|
Route::get('/orders/{id}/see', [AdminOrderController::class, 'see'])
|
||||||
->name('admin.orders.temp.add');
|
->name('orders.see');
|
||||||
|
|
||||||
|
|
||||||
// Route::get('/orders/{id}', [AdminOrderController::class, 'view'])
|
|
||||||
// ->name('admin.orders.view');
|
|
||||||
Route::get('/orders/{order:order_id}/see', [AdminOrderController::class, 'see'])
|
|
||||||
->name('admin.orders.see');
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// ORDERS (FIXED ROUTES)
|
// ORDERS (FIXED ROUTES)
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Add item to order
|
|
||||||
Route::post('/orders/{order}/item', [AdminOrderController::class, 'addItem'])
|
Route::post('/orders/{order}/item', [AdminOrderController::class, 'addItem'])
|
||||||
->name('admin.orders.addItem');
|
->name('admin.orders.addItem');
|
||||||
// Delete item from order
|
|
||||||
Route::delete('/orders/item/{id}', [AdminOrderController::class, 'deleteItem'])
|
Route::delete('/orders/item/{id}', [AdminOrderController::class, 'deleteItem'])
|
||||||
->name('admin.orders.deleteItem');
|
->name('admin.orders.deleteItem');
|
||||||
// Restore deleted item
|
|
||||||
Route::post('/orders/item/{id}/restore', [AdminOrderController::class, 'restoreItem'])
|
Route::post('/orders/item/{id}/restore', [AdminOrderController::class, 'restoreItem'])
|
||||||
->name('admin.orders.restoreItem');
|
->name('admin.orders.restoreItem');
|
||||||
// Update main order fields
|
|
||||||
Route::put('/admin/orders/item/update/{id}', [AdminOrderController::class, 'updateItem'])
|
Route::put('/orders/item/update/{id}', [AdminOrderController::class, 'updateItem'])
|
||||||
->name('admin.orders.updateItem');
|
->name('admin.orders.updateItem');
|
||||||
// Delete full order
|
|
||||||
Route::delete('/orders/{id}/delete', [AdminOrderController::class, 'destroy'])
|
Route::delete('/orders/{id}/delete', [AdminOrderController::class, 'destroy'])
|
||||||
->name('admin.orders.destroy');
|
->name('admin.orders.destroy');
|
||||||
|
|
||||||
|
|
||||||
Route::post('/orders/upload-excel-preview',
|
|
||||||
[AdminOrderController::class, 'uploadExcelPreview']
|
|
||||||
)->name('admin.orders.upload.excel.preview');
|
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// SHIPMENTS (FIXED ROUTES)
|
// SHIPMENTS (FIXED ROUTES)
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@@ -178,35 +226,21 @@ Route::prefix('admin')
|
|||||||
Route::post('/shipments/update-status', [ShipmentController::class, 'updateStatus'])
|
Route::post('/shipments/update-status', [ShipmentController::class, 'updateStatus'])
|
||||||
->name('admin.shipments.updateStatus');
|
->name('admin.shipments.updateStatus');
|
||||||
|
|
||||||
// Get shipment orders for modal (AJAX)
|
|
||||||
Route::get('/shipments/{id}/orders', [ShipmentController::class, 'getShipmentOrders'])
|
Route::get('/shipments/{id}/orders', [ShipmentController::class, 'getShipmentOrders'])
|
||||||
->name('admin.shipments.orders');
|
->name('admin.shipments.orders');
|
||||||
|
|
||||||
// Get shipment details for edit (AJAX)
|
|
||||||
Route::get('/shipments/{id}', [ShipmentController::class, 'show'])
|
Route::get('/shipments/{id}', [ShipmentController::class, 'show'])
|
||||||
->name('admin.shipments.show');
|
->name('admin.shipments.show');
|
||||||
|
|
||||||
// Shipment Update
|
|
||||||
Route::put('/shipments/{id}', [ShipmentController::class, 'update'])
|
Route::put('/shipments/{id}', [ShipmentController::class, 'update'])
|
||||||
->name('admin.shipments.update');
|
->name('admin.shipments.update');
|
||||||
|
|
||||||
// Shipment Delete
|
|
||||||
Route::delete('/shipments/{id}', [ShipmentController::class, 'destroy'])
|
Route::delete('/shipments/{id}', [ShipmentController::class, 'destroy'])
|
||||||
->name('admin.shipments.destroy');
|
->name('admin.shipments.destroy');
|
||||||
|
|
||||||
Route::get('/shipment/dummy/{id}', [ShipmentController::class, 'dummy'])
|
Route::get('/shipment/dummy/{id}', [ShipmentController::class, 'dummy'])
|
||||||
->name('admin.shipments.dummy');
|
->name('admin.shipments.dummy');
|
||||||
|
|
||||||
// web.php
|
|
||||||
Route::delete('/shipments/{shipment}/orders/{order}',
|
|
||||||
[ShipmentController::class, 'removeOrder']
|
|
||||||
)->name('admin.shipments.removeOrder');
|
|
||||||
|
|
||||||
Route::post('/shipments/{shipment}/add-orders',
|
|
||||||
[ShipmentController::class, 'addOrders']
|
|
||||||
)->name('admin.shipments.addOrders');
|
|
||||||
Route::get('/shipment/dummy/{id}', [ShipmentController::class, 'dummy'])
|
|
||||||
->name('admin.shipments.dummy');
|
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// INVOICES
|
// INVOICES
|
||||||
@@ -229,18 +263,17 @@ Route::prefix('admin')
|
|||||||
Route::post('/invoices/{id}/installment', [AdminInvoiceController::class, 'storeInstallment'])
|
Route::post('/invoices/{id}/installment', [AdminInvoiceController::class, 'storeInstallment'])
|
||||||
->name('admin.invoice.installment.store');
|
->name('admin.invoice.installment.store');
|
||||||
|
|
||||||
Route::get(
|
|
||||||
'/admin/invoices/{id}/download',
|
|
||||||
[AdminInvoiceController::class, 'downloadInvoice']
|
|
||||||
)->name('admin.invoices.download');
|
|
||||||
|
|
||||||
|
|
||||||
Route::delete('/installment/{id}', [AdminInvoiceController::class, 'deleteInstallment'])
|
Route::delete('/installment/{id}', [AdminInvoiceController::class, 'deleteInstallment'])
|
||||||
->name('admin.invoice.installment.delete');
|
->name('admin.invoice.installment.delete');
|
||||||
|
|
||||||
|
Route::put('admin/invoices/{invoice}/items', [AdminInvoiceController::class, 'updateItems'])
|
||||||
|
->name('admin.invoices.items.update');
|
||||||
|
|
||||||
// //Add New Invoice
|
Route::post('/admin/invoices/{invoice}/charge-group', [AdminInvoiceController::class, 'storeChargeGroup'])
|
||||||
Route::get('/admin/invoices/create', [InvoiceController::class, 'create'])->name('admin.invoices.create');
|
->name('admin.invoices.charge-group.store');
|
||||||
|
|
||||||
|
Route::get('/admin/invoices/create', [InvoiceController::class, 'create'])
|
||||||
|
->name('admin.invoices.create');
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@@ -262,24 +295,23 @@ Route::prefix('admin')
|
|||||||
->name('admin.customers.status');
|
->name('admin.customers.status');
|
||||||
|
|
||||||
|
|
||||||
// Chat list page
|
// CHAT
|
||||||
Route::get('/chat-support', [AdminChatController::class, 'index'])
|
Route::get('/chat-support', [AdminChatController::class, 'index'])
|
||||||
->name('admin.chat_support');
|
->name('admin.chat_support');
|
||||||
|
|
||||||
// Chat window (open specific user's chat)
|
|
||||||
Route::get('/chat-support/{ticketId}', [AdminChatController::class, 'openChat'])
|
Route::get('/chat-support/{ticketId}', [AdminChatController::class, 'openChat'])
|
||||||
->name('admin.chat.open');
|
->name('admin.chat.open');
|
||||||
|
|
||||||
// Admin sending message
|
|
||||||
Route::post('/chat-support/{ticketId}/send', [AdminChatController::class, 'sendMessage'])
|
Route::post('/chat-support/{ticketId}/send', [AdminChatController::class, 'sendMessage'])
|
||||||
->name('admin.chat.send');
|
->name('admin.chat.send');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// ADMIN ACCOUNT (AJAX) ROUTES
|
// ADMIN ACCOUNT (AJAX) ROUTES
|
||||||
// ==========================================
|
// ==========================================
|
||||||
Route::prefix('admin/account')
|
Route::prefix('admin/account')
|
||||||
->middleware(['web', 'auth:admin'])
|
->middleware('auth:admin')
|
||||||
->name('admin.account.')
|
->name('admin.account.')
|
||||||
->group(function () {
|
->group(function () {
|
||||||
|
|
||||||
@@ -304,15 +336,12 @@ Route::prefix('admin')
|
|||||||
Route::get('/entry/{entry_no}', [AdminAccountController::class, 'getEntryDetails'])
|
Route::get('/entry/{entry_no}', [AdminAccountController::class, 'getEntryDetails'])
|
||||||
->name('entry.details');
|
->name('entry.details');
|
||||||
|
|
||||||
// ⬇⬇ NEW ROUTES FOR EDIT + DELETE ⬇⬇
|
|
||||||
Route::post('/update-entry', [AdminAccountController::class, 'updateEntry'])
|
Route::post('/update-entry', [AdminAccountController::class, 'updateEntry'])
|
||||||
->name('update.entry');
|
->name('update.entry');
|
||||||
|
|
||||||
Route::post('/delete-entry', [AdminAccountController::class, 'deleteEntry'])
|
Route::post('/delete-entry', [AdminAccountController::class, 'deleteEntry'])
|
||||||
->name('delete.entry');
|
->name('delete.entry');
|
||||||
|
|
||||||
|
|
||||||
// ===== Associated Orders Routes (EDIT MODAL साठी) =====
|
|
||||||
Route::post('/add-orders-to-entry', [AdminAccountController::class, 'addOrdersToEntry'])
|
Route::post('/add-orders-to-entry', [AdminAccountController::class, 'addOrdersToEntry'])
|
||||||
->name('add.orders.to.entry');
|
->name('add.orders.to.entry');
|
||||||
|
|
||||||
@@ -321,13 +350,9 @@ Route::prefix('admin')
|
|||||||
|
|
||||||
Route::post('/remove-order-from-entry', [AdminAccountController::class, 'removeOrderFromEntry'])
|
Route::post('/remove-order-from-entry', [AdminAccountController::class, 'removeOrderFromEntry'])
|
||||||
->name('remove.order.from.entry');
|
->name('remove.order.from.entry');
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// REPORTS DOWNLOAD ROUTES
|
// REPORTS DOWNLOAD ROUTES
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@@ -337,21 +362,26 @@ Route::prefix('admin')
|
|||||||
Route::get('/admin/orders/download/excel', [AdminOrderController::class, 'downloadExcel'])
|
Route::get('/admin/orders/download/excel', [AdminOrderController::class, 'downloadExcel'])
|
||||||
->name('admin.orders.download.excel');
|
->name('admin.orders.download.excel');
|
||||||
|
|
||||||
|
Route::prefix('admin/account')->middleware('auth:admin')->name('admin.account.')->group(function () {
|
||||||
Route::prefix('admin/account')->middleware(['web', 'auth:admin'])->name('admin.account.')->group(function () {
|
|
||||||
Route::post('/toggle-payment', [AdminAccountController::class, 'togglePayment'])->name('toggle');
|
Route::post('/toggle-payment', [AdminAccountController::class, 'togglePayment'])->name('toggle');
|
||||||
});
|
});
|
||||||
|
|
||||||
//---------------------------
|
// STAFF RESOURCE
|
||||||
//Edit Button Route
|
Route::middleware(['auth:admin'])
|
||||||
//---------------------------
|
|
||||||
// protected admin routes
|
|
||||||
Route::middleware(['web', 'auth:admin'])
|
|
||||||
->prefix('admin')
|
->prefix('admin')
|
||||||
->name('admin.')
|
->name('admin.')
|
||||||
->group(function () {
|
->group(function () {
|
||||||
|
|
||||||
|
|
||||||
// staff resource
|
|
||||||
Route::resource('staff', AdminStaffController::class);
|
Route::resource('staff', AdminStaffController::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Extra admin prefix group (तसाच ठेवला)
|
||||||
|
Route::prefix('admin')->middleware('auth:admin')->group(function () {
|
||||||
|
// ... your routes
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::post('/admin/broadcasting/auth', function () {
|
||||||
|
return Broadcast::auth(request());
|
||||||
|
})->middleware('auth:admin');
|
||||||
|
|
||||||
|
Route::get('/admin/invoices/{invoice}/download', [InvoiceController::class, 'download'])
|
||||||
|
->name('admin.invoices.download');
|
||||||
|
|||||||
Reference in New Issue
Block a user