Compare commits
36 Commits
2741129740
...
dev
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
19d7f423b3 | ||
|
|
0257b68f16 | ||
|
|
785f2564be | ||
|
|
9a6ca49ad7 | ||
|
|
bf2689e62d | ||
|
|
c25b468c77 | ||
|
|
5d8a746876 | ||
|
|
bb2a361a97 | ||
|
|
6b5876e08f | ||
|
|
43b1a64911 | ||
|
|
ff4c006ca4 | ||
|
|
d5e9113820 | ||
|
|
bddbcf5c5f | ||
|
|
0c51ed1489 | ||
|
|
9cc6959396 | ||
|
|
c11467068c | ||
|
|
599023166a | ||
|
|
e188780329 | ||
|
|
338425535e | ||
|
|
f7856a6755 | ||
|
|
8f95091673 | ||
|
|
7362ef6bdc | ||
|
|
e872b83ea3 | ||
|
|
6ccf2cf84e | ||
|
|
4637f0b189 | ||
|
|
952dd7eddd | ||
|
|
451be1a533 | ||
|
|
cd9a786ef4 | ||
|
|
a6dd919d3f | ||
|
|
e0a8a5c69c | ||
|
|
7fa03688aa | ||
|
|
1885d3beef | ||
|
|
72a81fa111 | ||
|
|
044bfe5563 | ||
|
|
8ca8f05b93 | ||
|
|
ea2532efc8 |
14
.env.example
14
.env.example
@@ -1,6 +1,6 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_KEY=base64:/ulhRgiCOFjZV6xUDkXLfiR9X8iFRZ4QIiX3UJbdwY4=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
@@ -20,12 +20,12 @@ LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=kent_logistics6
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
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'));
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,10 @@ namespace App\Http\Controllers\Admin;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Models\Container;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\User;
|
||||
use App\Models\MarkList;
|
||||
|
||||
class AdminAuthController extends Controller
|
||||
{
|
||||
@@ -31,16 +35,15 @@ class AdminAuthController extends Controller
|
||||
}
|
||||
|
||||
$credentials = [
|
||||
$field => $loginInput,
|
||||
$field => $loginInput,
|
||||
'password' => $request->password,
|
||||
];
|
||||
|
||||
// attempt login
|
||||
if (Auth::guard('admin')->attempt($credentials)) {
|
||||
$request->session()->regenerate();
|
||||
$user = Auth::guard('admin')->user();
|
||||
|
||||
return redirect()->route('admin.dashboard')->with('success', 'Welcome back, ' . $user->name . '!');
|
||||
return redirect()->route('admin.dashboard')
|
||||
->with('success', 'Welcome back, ' . $user->name . '!');
|
||||
}
|
||||
|
||||
return back()->withErrors(['login' => 'Invalid login credentials.']);
|
||||
@@ -51,6 +54,25 @@ class AdminAuthController extends Controller
|
||||
Auth::guard('admin')->logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
return redirect()->route('admin.login')->with('success', 'Logged out successfully.');
|
||||
return redirect()->route('admin.login')
|
||||
->with('success', 'Logged out successfully.');
|
||||
}
|
||||
}
|
||||
|
||||
public function profile()
|
||||
{
|
||||
$user = Auth::guard('admin')->user();
|
||||
|
||||
// ── Real Stats ──
|
||||
$stats = [
|
||||
'total_containers' => Container::count(),
|
||||
'total_invoices' => Invoice::count(),
|
||||
'paid_invoices' => Invoice::where('status', 'paid')->count(),
|
||||
'pending_invoices' => Invoice::where('status', 'pending')->count(),
|
||||
'total_customers' => User::count(),
|
||||
'total_marklist' => MarkList::count(),
|
||||
'active_marklist' => MarkList::where('status', 'active')->count(),
|
||||
];
|
||||
|
||||
return view('admin.profile', compact('user', 'stats'));
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,8 @@ class AdminCustomerController extends Controller
|
||||
$query = User::with([
|
||||
'marks',
|
||||
'orders',
|
||||
'invoices.installments' // 🔥 IMPORTANT
|
||||
'invoices.installments',
|
||||
'invoices.chargeGroups', // 🔥 for order total calculation
|
||||
])->orderBy('id', 'desc');
|
||||
|
||||
if (!empty($search)) {
|
||||
@@ -159,4 +160,4 @@ class AdminCustomerController extends Controller
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,39 +2,70 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller; // ⬅️ हे नक्की असू दे
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use Mpdf\Mpdf;
|
||||
use App\Models\InvoiceChargeGroup;
|
||||
use App\Models\InvoiceChargeGroupItem;
|
||||
use App\Models\InvoiceInstallment;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Mpdf\Mpdf;
|
||||
|
||||
|
||||
class AdminInvoiceController extends Controller
|
||||
{
|
||||
// -------------------------------------------------------------
|
||||
// INVOICE LIST PAGE
|
||||
// INDEX (LIST ALL INVOICES WITH FILTERS)
|
||||
// -------------------------------------------------------------
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
$invoices = Invoice::with(['order.shipments'])->latest()->get();
|
||||
$query = Invoice::query();
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('invoice_number', 'like', "%{$search}%")
|
||||
->orWhere('customer_name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('status') && $request->status !== 'all') {
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
if ($request->filled('start_date')) {
|
||||
$query->whereDate('invoice_date', '>=', $request->start_date);
|
||||
}
|
||||
|
||||
if ($request->filled('end_date')) {
|
||||
$query->whereDate('invoice_date', '<=', $request->end_date);
|
||||
}
|
||||
|
||||
$invoices = $query->latest()->get();
|
||||
|
||||
return view('admin.invoice', compact('invoices'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// POPUP VIEW (AJAX)
|
||||
// POPUP VIEW
|
||||
// -------------------------------------------------------------
|
||||
public function popup($id)
|
||||
{
|
||||
$invoice = Invoice::with(['items', 'order'])->findOrFail($id);
|
||||
$invoice = Invoice::with([
|
||||
'items',
|
||||
'chargeGroups.items',
|
||||
])->findOrFail($id);
|
||||
|
||||
// Find actual Shipment record
|
||||
$shipment = \App\Models\Shipment::whereHas('items', function ($q) use ($invoice) {
|
||||
$q->where('order_id', $invoice->order_id);
|
||||
})
|
||||
->first();
|
||||
$shipment = null;
|
||||
|
||||
return view('admin.popup_invoice', compact('invoice', 'shipment'));
|
||||
$groupedItemIds = $invoice->chargeGroups
|
||||
->flatMap(fn($group) => $group->items->pluck('invoice_item_id'))
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return view('admin.popup_invoice', compact('invoice', 'shipment', 'groupedItemIds'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
@@ -42,90 +73,135 @@ class AdminInvoiceController extends Controller
|
||||
// -------------------------------------------------------------
|
||||
public function edit($id)
|
||||
{
|
||||
$invoice = Invoice::with(['order.shipments'])->findOrFail($id);
|
||||
$shipment = $invoice->order?->shipments?->first();
|
||||
$invoice = Invoice::with([
|
||||
'items',
|
||||
'customer',
|
||||
'container',
|
||||
'chargeGroups.items',
|
||||
'installments',
|
||||
])->findOrFail($id);
|
||||
|
||||
return view('admin.invoice_edit', compact('invoice', 'shipment'));
|
||||
// ✅ Customer details sync
|
||||
if ($invoice->customer) {
|
||||
$needsUpdate = [];
|
||||
|
||||
if (empty($invoice->customer_email) || $invoice->customer_email === 'test@demo.com') {
|
||||
$needsUpdate['customer_email'] = $invoice->customer->email;
|
||||
}
|
||||
if (empty($invoice->customer_address) || $invoice->customer_address === 'TEST ADDRESS') {
|
||||
$needsUpdate['customer_address'] = $invoice->customer->address;
|
||||
}
|
||||
if (empty($invoice->pincode) || $invoice->pincode === '999999') {
|
||||
$needsUpdate['pincode'] = $invoice->customer->pincode;
|
||||
}
|
||||
|
||||
if (!empty($needsUpdate)) {
|
||||
$invoice->update($needsUpdate);
|
||||
$invoice->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
$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 ONLY)
|
||||
// -------------------------------------------------------------
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
Log::info("🟡 Invoice Update Request Received", [
|
||||
Log::info('🟡 Invoice Update Request Received', [
|
||||
'invoice_id' => $id,
|
||||
'request' => $request->all()
|
||||
'request' => $request->all(),
|
||||
]);
|
||||
|
||||
$invoice = Invoice::findOrFail($id);
|
||||
|
||||
$data = $request->validate([
|
||||
'invoice_date' => 'required|date',
|
||||
'due_date' => 'required|date|after_or_equal:invoice_date',
|
||||
'final_amount' => 'required|numeric|min:0',
|
||||
'tax_type' => 'required|in:gst,igst',
|
||||
'tax_percent' => 'required|numeric|min:0|max:28',
|
||||
'status' => 'required|in:pending,paid,overdue',
|
||||
'notes' => 'nullable|string',
|
||||
'invoice_date' => 'required|date',
|
||||
'due_date' => 'required|date|after_or_equal:invoice_date',
|
||||
'status' => 'required|in:pending,paying,paid,overdue',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
|
||||
Log::info("✅ Validated Invoice Update Data", $data);
|
||||
|
||||
$finalAmount = floatval($data['final_amount']);
|
||||
$taxPercent = floatval($data['tax_percent']);
|
||||
$taxAmount = 0;
|
||||
|
||||
if ($data['tax_type'] === 'gst') {
|
||||
Log::info("🟢 GST Selected", compact('taxPercent'));
|
||||
$data['cgst_percent'] = $taxPercent / 2;
|
||||
$data['sgst_percent'] = $taxPercent / 2;
|
||||
$data['igst_percent'] = 0;
|
||||
} else {
|
||||
Log::info("🔵 IGST Selected", compact('taxPercent'));
|
||||
$data['cgst_percent'] = 0;
|
||||
$data['sgst_percent'] = 0;
|
||||
$data['igst_percent'] = $taxPercent;
|
||||
}
|
||||
|
||||
$taxAmount = ($finalAmount * $taxPercent) / 100;
|
||||
|
||||
$data['gst_amount'] = $taxAmount;
|
||||
$data['final_amount_with_gst'] = $finalAmount + $taxAmount;
|
||||
$data['gst_percent'] = $taxPercent;
|
||||
|
||||
Log::info("📌 Final Calculated Invoice Values", [
|
||||
'invoice_id' => $invoice->id,
|
||||
'final_amount' => $finalAmount,
|
||||
'gst_amount' => $data['gst_amount'],
|
||||
'final_amount_with_gst' => $data['final_amount_with_gst'],
|
||||
'tax_type' => $data['tax_type'],
|
||||
'cgst_percent' => $data['cgst_percent'],
|
||||
'sgst_percent' => $data['sgst_percent'],
|
||||
'igst_percent' => $data['igst_percent'],
|
||||
]);
|
||||
Log::info('✅ Validated Invoice Header Update Data', $data);
|
||||
|
||||
$invoice->update($data);
|
||||
$invoice->refresh();
|
||||
|
||||
Log::info("✅ Invoice Updated Successfully", [
|
||||
'invoice_id' => $invoice->id
|
||||
Log::info('🔍 Invoice AFTER HEADER UPDATE', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'charge_groups_total' => $invoice->charge_groups_total,
|
||||
'gst_amount' => $invoice->gst_amount,
|
||||
'grand_total_with_charges'=> $invoice->grand_total_with_charges,
|
||||
]);
|
||||
|
||||
// regenerate PDF
|
||||
$this->generateInvoicePDF($invoice);
|
||||
|
||||
return redirect()
|
||||
->route('admin.invoices.index')
|
||||
->route('admin.invoices.edit', $invoice->id)
|
||||
->with('success', 'Invoice updated & PDF generated successfully.');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PDF GENERATION USING mPDF
|
||||
// UPDATE INVOICE ITEMS (फक्त items save)
|
||||
// -------------------------------------------------------------
|
||||
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'],
|
||||
]);
|
||||
|
||||
foreach ($data['items'] 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();
|
||||
}
|
||||
|
||||
Log::info('✅ Invoice items updated (no totals recalculation)', [
|
||||
'invoice_id' => $invoice->id,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Invoice items updated successfully.');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PDF GENERATION (EXISTING - केवळ chargeGroups load ला confirm कर)
|
||||
// -------------------------------------------------------------
|
||||
public function generateInvoicePDF($invoice)
|
||||
{
|
||||
$invoice->load(['items', 'order.shipments']);
|
||||
$shipment = $invoice->order?->shipments?->first();
|
||||
// ✅ यामध्ये chargeGroups आणि installments load कर
|
||||
$invoice->load(['items', 'customer', 'container', 'chargeGroups.items', 'installments']);
|
||||
$shipment = null;
|
||||
|
||||
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
|
||||
$folder = public_path('invoices/');
|
||||
|
||||
@@ -134,98 +210,295 @@ class AdminInvoiceController extends Controller
|
||||
}
|
||||
|
||||
$filePath = $folder . $fileName;
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
$mpdf = new Mpdf(['mode' => 'utf-8', 'format' => 'A4', 'default_font' => 'sans-serif']);
|
||||
$html = view('admin.pdf.invoice', ['invoice' => $invoice, 'shipment' => $shipment])->render();
|
||||
$mpdf = new Mpdf([
|
||||
'mode' => 'utf-8',
|
||||
'format' => 'A4',
|
||||
'default_font' => 'sans-serif',
|
||||
]);
|
||||
|
||||
$html = view('admin.pdf.invoice', [
|
||||
'invoice' => $invoice,
|
||||
'shipment' => $shipment,
|
||||
])->render();
|
||||
|
||||
$mpdf->WriteHTML($html);
|
||||
$mpdf->Output($filePath, 'F');
|
||||
|
||||
$invoice->update(['pdf_path' => 'invoices/' . $fileName]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INSTALLMENTS (ADD/DELETE)
|
||||
// INSTALLMENTS (ADD)
|
||||
// -------------------------------------------------------------
|
||||
public function storeInstallment(Request $request, $invoice_id)
|
||||
{
|
||||
$request->validate([
|
||||
'installment_date' => 'required|date',
|
||||
'payment_method' => 'required|string',
|
||||
'reference_no' => 'nullable|string',
|
||||
'amount' => 'required|numeric|min:1',
|
||||
'payment_method' => 'required|string',
|
||||
'reference_no' => 'nullable|string',
|
||||
'amount' => 'required|numeric|min:1',
|
||||
]);
|
||||
|
||||
$invoice = Invoice::findOrFail($invoice_id);
|
||||
|
||||
$grandTotal = $invoice->grand_total_with_charges ?? 0;
|
||||
|
||||
$paidTotal = $invoice->installments()->sum('amount');
|
||||
// Use GST-inclusive total for all calculations/checks
|
||||
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
||||
$remaining = $grandTotal - $paidTotal;
|
||||
|
||||
if ($request->amount > $remaining) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Installment amount exceeds remaining balance.'
|
||||
'message' => 'Installment amount exceeds remaining balance.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$installment = InvoiceInstallment::create([
|
||||
'invoice_id' => $invoice_id,
|
||||
'invoice_id' => $invoice_id,
|
||||
'installment_date' => $request->installment_date,
|
||||
'payment_method' => $request->payment_method,
|
||||
'reference_no' => $request->reference_no,
|
||||
'amount' => $request->amount,
|
||||
'payment_method' => $request->payment_method,
|
||||
'reference_no' => $request->reference_no,
|
||||
'amount' => $request->amount,
|
||||
]);
|
||||
|
||||
$newPaid = $paidTotal + $request->amount;
|
||||
$remaining = max(0, $grandTotal - $newPaid);
|
||||
|
||||
// Mark as 'paid' if GST-inclusive total is cleared
|
||||
if ($newPaid >= $invoice->final_amount_with_gst) {
|
||||
$invoice->update(['status' => 'paid']);
|
||||
$isOverdue = now()->startOfDay()->gt(\Carbon\Carbon::parse($invoice->due_date)->startOfDay());
|
||||
|
||||
$this->generateInvoicePDF($invoice);
|
||||
if ($grandTotal > 0 && $newPaid >= $grandTotal) {
|
||||
$newStatus = 'paid';
|
||||
} elseif ($newPaid > 0 && $isOverdue) {
|
||||
$newStatus = 'overdue';
|
||||
} elseif ($newPaid > 0 && !$isOverdue) {
|
||||
$newStatus = 'paying';
|
||||
} elseif ($newPaid <= 0 && $isOverdue) {
|
||||
$newStatus = 'overdue';
|
||||
} else {
|
||||
$newStatus = 'pending';
|
||||
}
|
||||
|
||||
$invoice->update([
|
||||
'payment_method' => $request->payment_method,
|
||||
'reference_no' => $request->reference_no,
|
||||
'status' => $newStatus,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Installment added successfully.',
|
||||
'installment' => $installment,
|
||||
'totalPaid' => $newPaid,
|
||||
'gstAmount' => $invoice->gst_amount,
|
||||
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
||||
'baseAmount' => $invoice->final_amount,
|
||||
'remaining' => max(0, $invoice->final_amount_with_gst - $newPaid),
|
||||
'isCompleted' => $newPaid >= $invoice->final_amount_with_gst
|
||||
'status' => 'success',
|
||||
'message' => 'Installment added successfully.',
|
||||
'installment' => $installment,
|
||||
'chargeGroupsTotal' => $invoice->charge_groups_total ?? 0,
|
||||
'gstAmount' => $invoice->gst_amount ?? 0,
|
||||
'grandTotal' => $grandTotal,
|
||||
'totalPaid' => $newPaid,
|
||||
'remaining' => $remaining,
|
||||
'newStatus' => $newStatus,
|
||||
'isCompleted' => $remaining <= 0,
|
||||
'isZero' => $newPaid == 0,
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INSTALLMENTS (DELETE)
|
||||
// -------------------------------------------------------------
|
||||
public function deleteInstallment($id)
|
||||
{
|
||||
$installment = InvoiceInstallment::findOrFail($id);
|
||||
$invoice = $installment->invoice;
|
||||
|
||||
$installment->delete();
|
||||
|
||||
$paidTotal = $invoice->installments()->sum('amount');
|
||||
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
||||
|
||||
// Update status if not fully paid anymore
|
||||
if ($remaining > 0 && $invoice->status === "paid") {
|
||||
$invoice->update(['status' => 'pending']);
|
||||
|
||||
$this->generateInvoicePDF($invoice);
|
||||
$installment->delete();
|
||||
$invoice->refresh();
|
||||
|
||||
$grandTotal = $invoice->grand_total_with_charges ?? 0;
|
||||
|
||||
$paidTotal = $invoice->installments()->sum('amount');
|
||||
$remaining = max(0, $grandTotal - $paidTotal);
|
||||
|
||||
$isOverdue = now()->startOfDay()->gt(\Carbon\Carbon::parse($invoice->due_date)->startOfDay());
|
||||
|
||||
if ($grandTotal > 0 && $paidTotal >= $grandTotal) {
|
||||
$newStatus = 'paid';
|
||||
} elseif ($paidTotal > 0 && $isOverdue) {
|
||||
$newStatus = 'overdue';
|
||||
} elseif ($paidTotal > 0 && !$isOverdue) {
|
||||
$newStatus = 'paying';
|
||||
} elseif ($paidTotal <= 0 && $isOverdue) {
|
||||
$newStatus = 'overdue';
|
||||
} else {
|
||||
$newStatus = 'pending';
|
||||
}
|
||||
|
||||
|
||||
$invoice->update(['status' => $newStatus]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Installment deleted.',
|
||||
'totalPaid' => $paidTotal,
|
||||
'gstAmount' => $invoice->gst_amount,
|
||||
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
||||
'baseAmount' => $invoice->final_amount,
|
||||
'remaining' => $remaining,
|
||||
'isZero' => $paidTotal == 0
|
||||
'status' => 'success',
|
||||
'message' => 'Installment deleted.',
|
||||
'chargeGroupsTotal' => $invoice->charge_groups_total ?? 0,
|
||||
'gstAmount' => $invoice->gst_amount ?? 0,
|
||||
'grandTotal' => $grandTotal,
|
||||
'totalPaid' => $paidTotal,
|
||||
'remaining' => $remaining,
|
||||
'newStatus' => $newStatus,
|
||||
'isZero' => $paidTotal == 0,
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// CHARGE GROUP SAVE
|
||||
// -------------------------------------------------------------
|
||||
public function storeChargeGroup(Request $request, $invoiceId)
|
||||
{
|
||||
Log::info('🟡 storeChargeGroup HIT', [
|
||||
'invoice_id' => $invoiceId,
|
||||
'payload' => $request->all(),
|
||||
]);
|
||||
|
||||
$invoice = Invoice::with('items', 'chargeGroups')->findOrFail($invoiceId);
|
||||
|
||||
$data = $request->validate([
|
||||
'groupname' => 'required|string|max:255',
|
||||
'basistype' => 'required|in:ttl_qty,amount,ttl_cbm,ttl_kg',
|
||||
'basisvalue' => 'required|numeric',
|
||||
'rate' => 'required|numeric|min:0.0001',
|
||||
'autototal' => 'required|numeric|min:0.01',
|
||||
'itemids' => 'required|array',
|
||||
'itemids.*' => 'integer|exists:invoice_items,id',
|
||||
|
||||
'tax_type' => 'nullable|in:none,gst,igst',
|
||||
'gst_percent' => 'nullable|numeric|min:0|max:28',
|
||||
'total_with_gst' => 'nullable|numeric|min:0',
|
||||
]);
|
||||
|
||||
Log::info('✅ storeChargeGroup VALIDATED', $data);
|
||||
|
||||
$exists = InvoiceChargeGroup::where('invoice_id', $invoice->id)
|
||||
->where('group_name', $data['groupname'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return back()
|
||||
->withErrors(['groupname' => 'This group name is already used for this invoice.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
$taxType = $data['tax_type'] ?? 'gst';
|
||||
$gstPercent = $data['gst_percent'] ?? 0;
|
||||
$baseTotal = $data['autototal'];
|
||||
|
||||
$totalWithGst = $data['total_with_gst'] ?? $baseTotal;
|
||||
if ($totalWithGst == 0 && $gstPercent > 0) {
|
||||
$gstAmount = ($baseTotal * $gstPercent) / 100;
|
||||
$totalWithGst = $baseTotal + $gstAmount;
|
||||
}
|
||||
|
||||
$group = InvoiceChargeGroup::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
'group_name' => $data['groupname'],
|
||||
'basis_type' => $data['basistype'],
|
||||
'basis_value' => $data['basisvalue'],
|
||||
'rate' => $data['rate'],
|
||||
'total_charge' => $baseTotal,
|
||||
'tax_type' => $taxType,
|
||||
'gst_percent' => $gstPercent,
|
||||
'total_with_gst' => $totalWithGst,
|
||||
]);
|
||||
|
||||
foreach ($data['itemids'] as $itemId) {
|
||||
InvoiceChargeGroupItem::create([
|
||||
'group_id' => $group->id,
|
||||
'invoice_item_id' => $itemId,
|
||||
]);
|
||||
}
|
||||
|
||||
$invoice->load('chargeGroups');
|
||||
|
||||
$chargeGroupsBase = $invoice->chargeGroups->sum('total_charge');
|
||||
$chargeGroupsWithG = $invoice->chargeGroups->sum('total_with_gst');
|
||||
$chargeGroupsGst = $chargeGroupsWithG - $chargeGroupsBase;
|
||||
|
||||
$invoiceGstPercent = $group->gst_percent ?? 0;
|
||||
$invoiceTaxType = $group->tax_type ?? 'gst';
|
||||
|
||||
$cgstPercent = 0;
|
||||
$sgstPercent = 0;
|
||||
$igstPercent = 0;
|
||||
|
||||
if ($invoiceTaxType === 'gst') {
|
||||
$cgstPercent = $invoiceGstPercent / 2;
|
||||
$sgstPercent = $invoiceGstPercent / 2;
|
||||
} elseif ($invoiceTaxType === 'igst') {
|
||||
$igstPercent = $invoiceGstPercent;
|
||||
}
|
||||
|
||||
$invoice->update([
|
||||
'charge_groups_total' => $chargeGroupsBase,
|
||||
'gst_amount' => $chargeGroupsGst,
|
||||
'gst_percent' => $invoiceGstPercent,
|
||||
'tax_type' => $invoiceTaxType,
|
||||
'cgst_percent' => $cgstPercent,
|
||||
'sgst_percent' => $sgstPercent,
|
||||
'igst_percent' => $igstPercent,
|
||||
|
||||
'final_amount' => $chargeGroupsBase,
|
||||
'final_amount_with_gst' => $chargeGroupsWithG,
|
||||
'grand_total_with_charges' => $chargeGroupsWithG,
|
||||
]);
|
||||
|
||||
Log::info('✅ Invoice updated from Charge Group (CG total + GST)', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'charge_groups_total' => $chargeGroupsBase,
|
||||
'gst_amount' => $chargeGroupsGst,
|
||||
'grand_total_with_charges'=> $invoice->grand_total_with_charges,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Charge group saved successfully.',
|
||||
'group_id' => $group->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 🆕 PDF DOWNLOAD (Direct browser download)
|
||||
// ============================================
|
||||
public function downloadPdf($id)
|
||||
{
|
||||
$invoice = Invoice::with(['items', 'customer', 'container', 'chargeGroups.items', 'installments'])
|
||||
->findOrFail($id);
|
||||
|
||||
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
|
||||
$folder = public_path('invoices/');
|
||||
$filePath = $folder . $fileName;
|
||||
|
||||
// जर PDF exist नसेल तर generate कर
|
||||
if (!file_exists($filePath)) {
|
||||
$this->generateInvoicePDF($invoice);
|
||||
}
|
||||
|
||||
return response()->download($filePath, $fileName);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 🆕 EXCEL DOWNLOAD (CSV format - simple)
|
||||
// ============================================
|
||||
|
||||
|
||||
public function share($id)
|
||||
{
|
||||
$invoice = Invoice::findOrFail($id);
|
||||
|
||||
// इथे तुला जसं share करायचंय तसं logic टाक:
|
||||
// उदा. public link generate करून redirect कर, किंवा WhatsApp deeplink, इ.
|
||||
|
||||
$url = route('admin.invoices.popup', $invoice->id); // example: popup link share
|
||||
|
||||
return redirect()->away('https://wa.me/?text=' . urlencode($url));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,54 +3,199 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Order;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Mpdf\Mpdf;
|
||||
|
||||
class AdminReportController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the reports page with joined data
|
||||
*/
|
||||
public function index(Request $request)
|
||||
// UI साठी main action
|
||||
public function containerReport(Request $request)
|
||||
{
|
||||
// -------------------------------
|
||||
// FETCH REPORT DATA
|
||||
// ONLY orders that have BOTH:
|
||||
// 1. Invoice
|
||||
// 2. Shipment
|
||||
// -------------------------------
|
||||
$reports = DB::table('orders')
|
||||
->join('shipment_items', 'shipment_items.order_id', '=', 'orders.id')
|
||||
->join('shipments', 'shipments.id', '=', 'shipment_items.shipment_id')
|
||||
->join('invoices', 'invoices.order_id', '=', 'orders.id')
|
||||
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'orders.mark_no')
|
||||
->leftJoin('users', 'users.customer_id', '=', 'mark_list.customer_id')
|
||||
|
||||
->select(
|
||||
'orders.id as order_pk',
|
||||
'orders.order_id',
|
||||
'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_date',
|
||||
'invoices.final_amount',
|
||||
'invoices.status as invoice_status',
|
||||
|
||||
'mark_list.company_name',
|
||||
'mark_list.customer_name'
|
||||
)
|
||||
|
||||
->orderBy('shipments.shipment_date', 'desc')
|
||||
->get();
|
||||
$reports = $this->buildContainerReportQuery($request)->get();
|
||||
|
||||
return view('admin.reports', compact('reports'));
|
||||
}
|
||||
}
|
||||
|
||||
// ही common query — filters accept करते
|
||||
protected function buildContainerReportQuery(Request $request = null)
|
||||
{
|
||||
$query = DB::table('invoices')
|
||||
->join('containers', 'containers.id', '=', 'invoices.container_id')
|
||||
->leftJoinSub(
|
||||
DB::table('invoice_installments')
|
||||
->select('invoice_id', DB::raw('COALESCE(SUM(amount), 0) as total_paid'))
|
||||
->groupBy('invoice_id'),
|
||||
'inst',
|
||||
'inst.invoice_id',
|
||||
'=',
|
||||
'invoices.id'
|
||||
)
|
||||
->select(
|
||||
'containers.id as container_id',
|
||||
'containers.container_number',
|
||||
'containers.container_date',
|
||||
'containers.container_name',
|
||||
|
||||
DB::raw('COUNT(DISTINCT invoices.mark_no) as total_mark_nos'),
|
||||
DB::raw('COUNT(DISTINCT invoices.customer_id) as total_customers'),
|
||||
DB::raw('COUNT(invoices.id) as total_invoices'),
|
||||
|
||||
DB::raw('COALESCE(SUM(invoices.charge_groups_total), 0) as total_invoice_amount'),
|
||||
DB::raw('COALESCE(SUM(invoices.gst_amount), 0) as total_gst_amount'),
|
||||
DB::raw('COALESCE(SUM(invoices.grand_total_with_charges), 0) as total_payable'),
|
||||
DB::raw('COALESCE(SUM(inst.total_paid), 0) as total_paid'),
|
||||
DB::raw('GREATEST(0, COALESCE(SUM(invoices.grand_total_with_charges), 0) - COALESCE(SUM(inst.total_paid), 0)) as total_remaining'),
|
||||
|
||||
DB::raw("
|
||||
CASE
|
||||
WHEN COUNT(invoices.id) > 0
|
||||
AND SUM(CASE WHEN invoices.status != 'paid' THEN 1 ELSE 0 END) = 0
|
||||
THEN 'paid'
|
||||
WHEN SUM(CASE WHEN invoices.status = 'overdue' THEN 1 ELSE 0 END) > 0
|
||||
THEN 'overdue'
|
||||
WHEN SUM(CASE WHEN invoices.status = 'paying' THEN 1 ELSE 0 END) > 0
|
||||
THEN 'paying'
|
||||
ELSE 'pending'
|
||||
END as container_status
|
||||
")
|
||||
)
|
||||
->groupBy(
|
||||
'containers.id',
|
||||
'containers.container_number',
|
||||
'containers.container_date',
|
||||
'containers.container_name'
|
||||
)
|
||||
->orderBy('containers.container_date', 'desc')
|
||||
->orderBy('containers.id', 'desc');
|
||||
|
||||
// ── Filters ──────────────────────────────────────────────────────
|
||||
if ($request) {
|
||||
if ($request->filled('from_date')) {
|
||||
$query->whereDate('containers.container_date', '>=', $request->from_date);
|
||||
}
|
||||
|
||||
if ($request->filled('to_date')) {
|
||||
$query->whereDate('containers.container_date', '<=', $request->to_date);
|
||||
}
|
||||
|
||||
if ($request->filled('status')) {
|
||||
// container_status हे aggregate expression आहे,
|
||||
// त्यामुळे HAVING clause वापरतो
|
||||
$query->havingRaw("
|
||||
CASE
|
||||
WHEN COUNT(invoices.id) > 0
|
||||
AND SUM(CASE WHEN invoices.status != 'paid' THEN 1 ELSE 0 END) = 0
|
||||
THEN 'paid'
|
||||
WHEN SUM(CASE WHEN invoices.status = 'overdue' THEN 1 ELSE 0 END) > 0
|
||||
THEN 'overdue'
|
||||
WHEN SUM(CASE WHEN invoices.status = 'paying' THEN 1 ELSE 0 END) > 0
|
||||
THEN 'paying'
|
||||
ELSE 'pending'
|
||||
END = ?
|
||||
", [$request->status]);
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
// ---- Excel export ----
|
||||
public function containerReportExcel(Request $request)
|
||||
{
|
||||
$reports = $this->buildContainerReportQuery($request)->get();
|
||||
|
||||
$headings = [
|
||||
'Container No',
|
||||
'Container Date',
|
||||
'Total Mark Nos',
|
||||
'Total Customers',
|
||||
'Total Invoices',
|
||||
'Invoice Amount (Before GST)',
|
||||
'GST Amount',
|
||||
'Payable Amount (Incl. GST)',
|
||||
'Paid Amount',
|
||||
'Remaining Amount',
|
||||
'Container Status',
|
||||
];
|
||||
|
||||
$rows = $reports->map(function ($r) {
|
||||
return [
|
||||
$r->container_number,
|
||||
$r->container_date,
|
||||
$r->total_mark_nos,
|
||||
$r->total_customers,
|
||||
$r->total_invoices,
|
||||
$r->total_invoice_amount,
|
||||
$r->total_gst_amount,
|
||||
$r->total_payable,
|
||||
$r->total_paid,
|
||||
$r->total_remaining,
|
||||
$r->container_status,
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
$export = new class($headings, $rows) implements
|
||||
\Maatwebsite\Excel\Concerns\FromArray,
|
||||
\Maatwebsite\Excel\Concerns\WithHeadings
|
||||
{
|
||||
private $headings;
|
||||
private $rows;
|
||||
|
||||
public function __construct($headings, $rows)
|
||||
{
|
||||
$this->headings = $headings;
|
||||
$this->rows = $rows;
|
||||
}
|
||||
|
||||
public function array(): array
|
||||
{
|
||||
return $this->rows;
|
||||
}
|
||||
|
||||
public function headings(): array
|
||||
{
|
||||
return $this->headings;
|
||||
}
|
||||
};
|
||||
|
||||
return Excel::download(
|
||||
$export,
|
||||
'container-report-' . now()->format('Ymd-His') . '.xlsx'
|
||||
);
|
||||
}
|
||||
|
||||
// ---- PDF export ----
|
||||
public function containerReportPdf(Request $request)
|
||||
{
|
||||
$reports = $this->buildContainerReportQuery($request)->get();
|
||||
|
||||
$html = view('admin.reports', [
|
||||
'reports' => $reports,
|
||||
'isPdf' => true,
|
||||
])->render();
|
||||
|
||||
$mpdf = new \Mpdf\Mpdf([
|
||||
'mode' => 'utf-8',
|
||||
'format' => 'A4-L',
|
||||
'default_font' => 'dejavusans',
|
||||
'margin_top' => 8,
|
||||
'margin_right' => 8,
|
||||
'margin_bottom' => 10,
|
||||
'margin_left' => 8,
|
||||
]);
|
||||
|
||||
$mpdf->SetHTMLHeader('');
|
||||
$mpdf->SetHTMLFooter('');
|
||||
|
||||
$mpdf->WriteHTML($html);
|
||||
|
||||
$fileName = 'container-report-' . now()->format('Ymd-His') . '.pdf';
|
||||
|
||||
return response($mpdf->Output($fileName, 'S'), 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,11 @@ class ShipmentController extends Controller
|
||||
$usedOrderIds = ShipmentItem::pluck('order_id')->toArray();
|
||||
|
||||
// 2) Load available orders (not used in any shipment)
|
||||
$availableOrders = Order::whereNotIn('id', $usedOrderIds)->get();
|
||||
$availableOrders = Order::whereNotIn('id', $usedOrderIds)
|
||||
->where('status', '!=', 'order_placed')
|
||||
->get();
|
||||
|
||||
|
||||
|
||||
// 3) Load all shipments for listing
|
||||
$shipments = Shipment::latest()->get();
|
||||
@@ -65,6 +69,16 @@ class ShipmentController extends Controller
|
||||
// CALCULATE TOTALS
|
||||
// -----------------------------
|
||||
$orders = Order::whereIn('id', $request->order_ids)->get();
|
||||
foreach ($orders as $order) {
|
||||
if ($order->status === 'order_placed') {
|
||||
return back()->with(
|
||||
'error',
|
||||
"Order {$order->order_id} is not ready for shipment"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
$total_ctn = $orders->sum('ctn');
|
||||
$total_qty = $orders->sum('qty');
|
||||
@@ -82,7 +96,7 @@ class ShipmentController extends Controller
|
||||
'shipment_id' => $newShipmentId,
|
||||
'origin' => $request->origin,
|
||||
'destination' => $request->destination,
|
||||
'status' => Shipment::STATUS_PENDING,
|
||||
'status' => Shipment::STATUS_SHIPMENT_READY,
|
||||
'shipment_date' => $request->shipment_date,
|
||||
|
||||
'total_ctn' => $total_ctn,
|
||||
@@ -135,29 +149,35 @@ class ShipmentController extends Controller
|
||||
* Update Shipment status from action button
|
||||
*/
|
||||
public function updateStatus(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'shipment_id' => 'required|exists:shipments,id',
|
||||
'status' => 'required|string'
|
||||
]);
|
||||
{
|
||||
$request->validate([
|
||||
'shipment_id' => 'required|exists:shipments,id',
|
||||
'status' => 'required|string'
|
||||
]);
|
||||
|
||||
// 1) Update shipment status
|
||||
$shipment = Shipment::findOrFail($request->shipment_id);
|
||||
$shipment->status = $request->status;
|
||||
$shipment->save();
|
||||
$shipment = Shipment::findOrFail($request->shipment_id);
|
||||
$shipment->status = $request->status;
|
||||
$shipment->save();
|
||||
|
||||
// 2) Update ALL related orders' status
|
||||
foreach ($shipment->orders as $order) {
|
||||
$order->status = $shipment->status; // status is string: pending, in_transit, dispatched, delivered
|
||||
$order->save();
|
||||
// ✅ Sync shipment status to orders ONLY after shipment exists
|
||||
foreach ($shipment->orders as $order) {
|
||||
|
||||
// Prevent rollback or overwrite
|
||||
if ($order->status === 'delivered') {
|
||||
continue;
|
||||
}
|
||||
|
||||
return redirect()->back()->with(
|
||||
'success',
|
||||
"Shipment status updated to {$shipment->statusLabel()} and related orders updated."
|
||||
);
|
||||
$order->status = $shipment->status;
|
||||
$order->save();
|
||||
}
|
||||
|
||||
return redirect()->back()->with(
|
||||
'success',
|
||||
"Shipment status updated to {$shipment->statusLabel()}."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update shipment details
|
||||
*/
|
||||
@@ -266,11 +286,23 @@ public function addOrders(Request $request, Shipment $shipment)
|
||||
'order_ids' => 'required|array|min:1',
|
||||
]);
|
||||
|
||||
// फक्त न वापरलेले orders घ्या
|
||||
$orders = Order::whereIn('id', $request->order_ids)->get();
|
||||
|
||||
foreach ($orders as $order) {
|
||||
// pivot मध्ये insert
|
||||
|
||||
if ($order->status === 'order_placed') {
|
||||
return back()->with(
|
||||
'error',
|
||||
"Order {$order->order_id} is not ready for shipment"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Prevent duplicates
|
||||
if (ShipmentItem::where('order_id', $order->id)->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ShipmentItem::create([
|
||||
'shipment_id' => $shipment->id,
|
||||
'order_id' => $order->id,
|
||||
@@ -282,23 +314,25 @@ public function addOrders(Request $request, Shipment $shipment)
|
||||
]);
|
||||
}
|
||||
|
||||
// totals
|
||||
// Recalculate totals
|
||||
$orderIds = ShipmentItem::where('shipment_id', $shipment->id)->pluck('order_id');
|
||||
$allOrders = Order::whereIn('id', $orderIds)->get();
|
||||
|
||||
$shipment->total_ctn = $allOrders->sum('ctn');
|
||||
$shipment->total_qty = $allOrders->sum('qty');
|
||||
$shipment->total_ttl_qty = $allOrders->sum('ttl_qty');
|
||||
$shipment->total_cbm = $allOrders->sum('cbm');
|
||||
$shipment->total_ttl_cbm = $allOrders->sum('ttl_cbm');
|
||||
$shipment->total_kg = $allOrders->sum('kg');
|
||||
$shipment->total_ttl_kg = $allOrders->sum('ttl_kg');
|
||||
$shipment->total_amount = $allOrders->sum('ttl_amount');
|
||||
$shipment->save();
|
||||
$shipment->update([
|
||||
'total_ctn' => $allOrders->sum('ctn'),
|
||||
'total_qty' => $allOrders->sum('qty'),
|
||||
'total_ttl_qty' => $allOrders->sum('ttl_qty'),
|
||||
'total_cbm' => $allOrders->sum('cbm'),
|
||||
'total_ttl_cbm' => $allOrders->sum('ttl_cbm'),
|
||||
'total_kg' => $allOrders->sum('kg'),
|
||||
'total_ttl_kg' => $allOrders->sum('ttl_kg'),
|
||||
'total_amount' => $allOrders->sum('ttl_amount'),
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.shipments.dummy', $shipment->id)
|
||||
->with('success', 'Orders added to shipment successfully.');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -85,7 +85,6 @@ public function approveProfileUpdate($id)
|
||||
$req = \App\Models\UpdateRequest::findOrFail($id);
|
||||
$user = \App\Models\User::findOrFail($req->user_id);
|
||||
|
||||
// FIX: Ensure data is array
|
||||
$newData = is_array($req->data) ? $req->data : json_decode($req->data, true);
|
||||
|
||||
foreach ($newData as $key => $value) {
|
||||
@@ -96,8 +95,18 @@ public function approveProfileUpdate($id)
|
||||
}
|
||||
}
|
||||
|
||||
// Update user table
|
||||
$user->save();
|
||||
|
||||
// Update mark_list table
|
||||
\App\Models\MarkList::where('customer_id', $user->customer_id)
|
||||
->update([
|
||||
'customer_name' => $user->customer_name,
|
||||
'company_name' => $user->company_name,
|
||||
'mobile_no' => $user->mobile_no
|
||||
]);
|
||||
|
||||
// Update request status
|
||||
$req->status = 'approved';
|
||||
$req->admin_note = 'Approved by admin on ' . now();
|
||||
$req->save();
|
||||
|
||||
964
app/Http/Controllers/ContainerController.php
Normal file
964
app/Http/Controllers/ContainerController.php
Normal file
@@ -0,0 +1,964 @@
|
||||
<?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;
|
||||
use Carbon\Carbon;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
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,
|
||||
'shopno_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;
|
||||
} elseif (
|
||||
strpos($normalized, 'SHOPNO') !== false ||
|
||||
strpos($normalized, 'SHOP') !== false
|
||||
) {
|
||||
$essentialColumns['shopno_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
|
||||
$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']] ?? '') : '';
|
||||
|
||||
$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)) {
|
||||
$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
|
||||
$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;
|
||||
|
||||
$customerUser = \App\Models\User::where('customer_id', $customerId)->first();
|
||||
|
||||
$invoice = new Invoice();
|
||||
$invoice->container_id = $container->id;
|
||||
$invoice->customer_id = $customerUser->id ?? null;
|
||||
$invoice->mark_no = $firstMark;
|
||||
|
||||
$invoice->invoice_number = $this->generateInvoiceNumber();
|
||||
|
||||
$invoice->invoice_date = $container->container_date;
|
||||
|
||||
$invoice->due_date = Carbon::parse($invoice->invoice_date)
|
||||
->addDays(10)
|
||||
->format('Y-m-d');
|
||||
|
||||
if ($snap) {
|
||||
$invoice->customer_name = $snap['customer_name'] ?? null;
|
||||
$invoice->company_name = $snap['company_name'] ?? null;
|
||||
$invoice->customer_mobile = $snap['mobile_no'] ?? null;
|
||||
}
|
||||
|
||||
if ($customerUser) {
|
||||
$invoice->customer_email = $customerUser->email ?? null;
|
||||
$invoice->customer_address = $customerUser->address ?? null;
|
||||
$invoice->pincode = $customerUser->pincode ?? null;
|
||||
}
|
||||
|
||||
$invoice->final_amount = 0;
|
||||
$invoice->gst_percent = 0;
|
||||
$invoice->gst_amount = 0;
|
||||
$invoice->final_amount_with_gst = 0;
|
||||
|
||||
$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++;
|
||||
|
||||
foreach ($rowsForCustomer as $item) {
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
$mark = $item['mark']; // ✅ mark_no from Excel
|
||||
|
||||
$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;
|
||||
|
||||
$shopNo = $essentialColumns['shopno_col'] !== null ? ($row[$essentialColumns['shopno_col']] ?? null) : null;
|
||||
|
||||
$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' => $shopNo,
|
||||
'mark_no' => $mark, // ✅ save mark_no from Excel
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$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');
|
||||
|
||||
$lockedRowIndexes = \App\Models\Invoice::whereIn('invoices.status', ['paid', 'paying'])
|
||||
->where('invoices.container_id', $container->id)
|
||||
->join('invoice_items', 'invoices.id', '=', 'invoice_items.invoice_id')
|
||||
->pluck('invoice_items.container_row_index')
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return view('admin.container_show', compact('container', 'lockedRowIndexes'));
|
||||
}
|
||||
|
||||
public function updateRows(Request $request, Container $container)
|
||||
{
|
||||
$rowsInput = $request->input('rows', []);
|
||||
|
||||
foreach ($rowsInput as $rowId => $cols) {
|
||||
$row = ContainerRow::where('container_id', $container->id)
|
||||
->where('id', $rowId)
|
||||
->first();
|
||||
|
||||
if (!$row) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = $row->data ?? [];
|
||||
foreach ($cols as $colHeader => $value) {
|
||||
$data[$colHeader] = $value;
|
||||
}
|
||||
$row->update(['data' => $data]);
|
||||
|
||||
$normalizedMap = [];
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key === null || $key === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normKey = strtoupper((string)$key);
|
||||
$normKey = str_replace([' ', '/', '-', '.'], '', $normKey);
|
||||
$normalizedMap[$normKey] = $value;
|
||||
}
|
||||
|
||||
$getFirstNumeric = function (array $map, array $possibleKeys) {
|
||||
foreach ($possibleKeys as $search) {
|
||||
$normSearch = strtoupper($search);
|
||||
$normSearch = str_replace([' ', '/', '-', '.'], '', $normSearch);
|
||||
|
||||
foreach ($map as $nKey => $value) {
|
||||
if (strpos($nKey, $normSearch) !== false) {
|
||||
if (is_numeric($value)) {
|
||||
return (float)$value;
|
||||
}
|
||||
if (is_string($value) && is_numeric(trim($value))) {
|
||||
return (float)trim($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
$ctnKeys = ['CTN', 'CTNS'];
|
||||
$qtyKeys = ['QTY', 'PCS', 'PIECES'];
|
||||
$ttlQtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY'];
|
||||
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
|
||||
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
|
||||
$amountKeys = ['AMOUNT', 'TTLAMOUNT', 'TOTALAMOUNT'];
|
||||
|
||||
$ctn = $getFirstNumeric($normalizedMap, $ctnKeys);
|
||||
|
||||
$qty = $getFirstNumeric($normalizedMap, $qtyKeys);
|
||||
|
||||
$ttlQ = $getFirstNumeric($normalizedMap, $ttlQtyKeys);
|
||||
|
||||
if ($ttlQ == 0 && $ctn && $qty) {
|
||||
$ttlQ = $ctn * $qty;
|
||||
}
|
||||
|
||||
$cbm = $getFirstNumeric($normalizedMap, ['CBM']);
|
||||
$ttlC = $getFirstNumeric($normalizedMap, ['TOTALCBM', 'TTLCBM', 'ITLCBM']);
|
||||
if ($ttlC == 0 && $cbm && $ctn) {
|
||||
$ttlC = $cbm * $ctn;
|
||||
}
|
||||
|
||||
$kg = $getFirstNumeric($normalizedMap, ['KG', 'WEIGHT']);
|
||||
$ttlK = $getFirstNumeric($normalizedMap, ['TOTALKG', 'TTKG']);
|
||||
if ($ttlK == 0 && $kg && $ctn) {
|
||||
$ttlK = $kg * $ctn;
|
||||
}
|
||||
|
||||
$price = $getFirstNumeric($normalizedMap, ['PRICE', 'RATE']);
|
||||
$amount = $getFirstNumeric($normalizedMap, $amountKeys);
|
||||
if ($amount == 0 && $price && $ttlQ) {
|
||||
$amount = $price * $ttlQ;
|
||||
}
|
||||
|
||||
$desc = null;
|
||||
foreach (['DESCRIPTION', 'DESC'] as $dKey) {
|
||||
$normD = str_replace([' ', '/', '-', '.'], '', strtoupper($dKey));
|
||||
foreach ($normalizedMap as $nKey => $v) {
|
||||
if (strpos($nKey, $normD) !== false) {
|
||||
$desc = is_string($v) ? trim($v) : $v;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$shopNo = null;
|
||||
foreach (['SHOPNO', 'SHOP'] as $sKey) {
|
||||
$normS = str_replace([' ', '/', '-', '.'], '', strtoupper($sKey));
|
||||
foreach ($normalizedMap as $nKey => $v) {
|
||||
if (strpos($nKey, $normS) !== false) {
|
||||
$shopNo = is_string($v) ? trim($v) : $v;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Get mark_no
|
||||
$markNo = null;
|
||||
foreach (['MARKNO', 'MARK', 'ITEMNO', 'ITEM'] as $mKey) {
|
||||
$normM = str_replace([' ', '/', '-', '.'], '', strtoupper($mKey));
|
||||
foreach ($normalizedMap as $nKey => $v) {
|
||||
if (strpos($nKey, $normM) !== false) {
|
||||
$markNo = is_string($v) ? trim($v) : $v;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rowIndex = $row->row_index;
|
||||
|
||||
$items = InvoiceItem::where('container_id', $container->id)
|
||||
->where('container_row_index', $rowIndex)
|
||||
->get();
|
||||
|
||||
if ($items->isEmpty() && $desc) {
|
||||
$items = InvoiceItem::where('container_id', $container->id)
|
||||
->whereNull('container_row_index')
|
||||
->where('description', $desc)
|
||||
->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->shop_no = $shopNo;
|
||||
$item->mark_no = $markNo; // ✅ update mark_no
|
||||
$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.');
|
||||
}
|
||||
|
||||
public function updateStatus(Request $request, Container $container)
|
||||
{
|
||||
$request->validate([
|
||||
'status' => 'required|in:container-ready,export-custom,international-transit,arrived-at-india,import-custom,warehouse,domestic-distribution,out-for-delivery,delivered',
|
||||
]);
|
||||
|
||||
$container->status = $request->status;
|
||||
$container->save();
|
||||
|
||||
if ($request->wantsJson() || $request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'status' => $container->status,
|
||||
]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Container status updated.');
|
||||
}
|
||||
|
||||
public function destroy(Container $container)
|
||||
{
|
||||
$container->delete();
|
||||
|
||||
if (request()->wantsJson() || request()->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Container deleted',
|
||||
]);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public function downloadPdf(Container $container)
|
||||
{
|
||||
$container->load('rows');
|
||||
|
||||
$pdf = Pdf::loadView('admin.container_pdf', [
|
||||
'container' => $container,
|
||||
])->setPaper('a4', 'landscape');
|
||||
|
||||
$fileName = 'container-'.$container->container_number.'.pdf';
|
||||
|
||||
return $pdf->download($fileName);
|
||||
}
|
||||
|
||||
public function downloadExcel(Container $container)
|
||||
{
|
||||
if (!$container->excel_file) {
|
||||
abort(404, 'Excel file not found on record.');
|
||||
}
|
||||
|
||||
$path = $container->excel_file;
|
||||
|
||||
if (!Storage::exists($path)) {
|
||||
abort(404, 'Excel file missing on server.');
|
||||
}
|
||||
|
||||
$fileName = 'container-'.$container->container_number.'.xlsx';
|
||||
|
||||
return Storage::download($path, $fileName);
|
||||
}
|
||||
|
||||
public function popupPopup(Container $container)
|
||||
{
|
||||
$container->load('rows');
|
||||
|
||||
$rows = $container->rows ?? collect();
|
||||
|
||||
$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) continue;
|
||||
$normKey = strtoupper((string)$key);
|
||||
$normKey = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normKey);
|
||||
$normalizedMap[$normKey] = $value;
|
||||
}
|
||||
|
||||
foreach ($possibleKeys as $search) {
|
||||
$normSearch = strtoupper($search);
|
||||
$normSearch = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normSearch);
|
||||
|
||||
foreach ($normalizedMap as $nKey => $value) {
|
||||
if (strpos($nKey, $normSearch) !== false) {
|
||||
if (is_numeric($value)) {
|
||||
return (float)$value;
|
||||
}
|
||||
if (is_string($value) && is_numeric(trim($value))) {
|
||||
return (float)trim($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$data = $row->data ?? [];
|
||||
if (!is_array($data)) continue;
|
||||
|
||||
$totalCtn += $getFirstNumeric($data, $ctnKeys);
|
||||
$totalQty += $getFirstNumeric($data, $qtyKeys);
|
||||
$totalCbm += $getFirstNumeric($data, $cbmKeys);
|
||||
$totalKg += $getFirstNumeric($data, $kgKeys);
|
||||
}
|
||||
|
||||
$summary = [
|
||||
'total_ctn' => round($totalCtn, 2),
|
||||
'total_qty' => round($totalQty, 2),
|
||||
'total_cbm' => round($totalCbm, 3),
|
||||
'total_kg' => round($totalKg, 2),
|
||||
];
|
||||
|
||||
return view('admin.partials.container_popup_readonly', [
|
||||
'container' => $container,
|
||||
'summary' => $summary,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -21,23 +21,33 @@ class UserOrderController extends Controller
|
||||
}
|
||||
|
||||
// -------------------------------------
|
||||
// Get all orders
|
||||
// Get customer invoices with containers
|
||||
// -------------------------------------
|
||||
$orders = $user->orders()->with('invoice')->get();
|
||||
$invoices = $user->invoices()->with('container')->get();
|
||||
|
||||
// Unique containers for this customer
|
||||
$containers = $invoices->pluck('container')->filter()->unique('id');
|
||||
|
||||
// -------------------------------------
|
||||
// Counts
|
||||
// Counts based on container status
|
||||
// -------------------------------------
|
||||
$totalOrders = $orders->count();
|
||||
$delivered = $orders->where('status', 'delivered')->count();
|
||||
$inTransit = $orders->where('status', '!=', 'delivered')->count();
|
||||
$active = $totalOrders;
|
||||
$totalOrders = $containers->count();
|
||||
|
||||
$delivered = $containers->where('status', 'delivered')->count();
|
||||
|
||||
$inTransit = $containers->whereNotIn('status', [
|
||||
'delivered',
|
||||
'warehouse',
|
||||
'domestic-distribution'
|
||||
])->count();
|
||||
|
||||
$active = $totalOrders;
|
||||
|
||||
// -------------------------------------
|
||||
// Total Amount = Invoice.total_with_gst
|
||||
// Total Amount = sum of invoice totals
|
||||
// -------------------------------------
|
||||
$totalAmount = $orders->sum(function ($o) {
|
||||
return $o->invoice->final_amount_with_gst ?? 0;
|
||||
$totalAmount = $invoices->sum(function ($invoice) {
|
||||
return $invoice->final_amount_with_gst ?? 0;
|
||||
});
|
||||
|
||||
// Format total amount in K, L, Cr
|
||||
@@ -45,13 +55,12 @@ class UserOrderController extends Controller
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
|
||||
'summary' => [
|
||||
'active_orders' => $active,
|
||||
'in_transit_orders' => $inTransit,
|
||||
'delivered_orders' => $delivered,
|
||||
'total_value' => $formattedAmount, // formatted value
|
||||
'total_raw' => $totalAmount // original value
|
||||
'total_value' => $formattedAmount,
|
||||
'total_raw' => $totalAmount
|
||||
]
|
||||
]);
|
||||
}
|
||||
@@ -90,20 +99,28 @@ class UserOrderController extends Controller
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Fetch orders for this user
|
||||
$orders = $user->orders()
|
||||
->with(['invoice', 'shipments'])
|
||||
// Get invoices with containers for this customer
|
||||
$invoices = $user->invoices()
|
||||
->with('container')
|
||||
->orderBy('id', 'desc')
|
||||
->get()
|
||||
->map(function ($o) {
|
||||
return [
|
||||
'order_id' => $o->order_id,
|
||||
'status' => $o->status,
|
||||
'amount' => $o->ttl_amount,
|
||||
'description'=> "Order from {$o->origin} to {$o->destination}",
|
||||
'created_at' => $o->created_at,
|
||||
];
|
||||
});
|
||||
->get();
|
||||
|
||||
// Extract unique containers
|
||||
$containers = $invoices->pluck('container')
|
||||
->filter()
|
||||
->unique('id')
|
||||
->values();
|
||||
|
||||
$orders = $containers->map(function ($container) {
|
||||
|
||||
return [
|
||||
'order_id' => $container->id,
|
||||
'container_number' => $container->container_number,
|
||||
'status' => $container->status,
|
||||
'container_date' => $container->container_date,
|
||||
'created_at' => $container->created_at,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
@@ -115,45 +132,73 @@ public function orderDetails($order_id)
|
||||
{
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
$order = $user->orders()
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Find container first
|
||||
$container = \App\Models\Container::find($order_id);
|
||||
|
||||
if (!$container) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Container not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Find invoice belonging to this user for this container
|
||||
$invoice = \App\Models\Invoice::where('customer_id', $user->id)
|
||||
->where('container_id', $container->id)
|
||||
->with(['items'])
|
||||
->where('order_id', $order_id)
|
||||
->first();
|
||||
|
||||
if (!$order) {
|
||||
return response()->json(['success' => false, 'message' => 'Order not found'], 404);
|
||||
if (!$invoice) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Order not found for this user'
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'order' => $order
|
||||
'order' => [
|
||||
'container_id' => $container->id,
|
||||
'container_number' => $container->container_number,
|
||||
'container_date' => $container->container_date,
|
||||
'status' => $container->status,
|
||||
'invoice_id' => $invoice->id,
|
||||
'items' => $invoice->items
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function orderShipment($order_id)
|
||||
{
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
// public function orderShipment($order_id)
|
||||
// {
|
||||
// $user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
// Get order
|
||||
$order = $user->orders()->where('order_id', $order_id)->first();
|
||||
// // Get order
|
||||
// $order = $user->orders()->where('order_id', $order_id)->first();
|
||||
|
||||
if (!$order) {
|
||||
return response()->json(['success' => false, 'message' => 'Order not found'], 404);
|
||||
}
|
||||
// if (!$order) {
|
||||
// return response()->json(['success' => false, 'message' => 'Order not found'], 404);
|
||||
// }
|
||||
|
||||
// Find shipment only for this order
|
||||
$shipment = $order->shipments()
|
||||
->with(['items' => function ($q) use ($order) {
|
||||
$q->where('order_id', $order->id);
|
||||
}])
|
||||
->first();
|
||||
// // Find shipment only for this order
|
||||
// $shipment = $order->shipments()
|
||||
// ->with(['items' => function ($q) use ($order) {
|
||||
// $q->where('order_id', $order->id);
|
||||
// }])
|
||||
// ->first();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'shipment' => $shipment
|
||||
]);
|
||||
}
|
||||
// return response()->json([
|
||||
// 'success' => true,
|
||||
// 'shipment' => $shipment
|
||||
// ]);
|
||||
// }
|
||||
|
||||
|
||||
public function orderInvoice($order_id)
|
||||
@@ -179,23 +224,35 @@ public function trackOrder($order_id)
|
||||
{
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
$order = $user->orders()
|
||||
->with('shipments')
|
||||
->where('order_id', $order_id)
|
||||
->first();
|
||||
|
||||
if (!$order) {
|
||||
return response()->json(['success' => false, 'message' => 'Order not found'], 404);
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 401);
|
||||
}
|
||||
|
||||
$shipment = $order->shipments()->first();
|
||||
// Ensure the container belongs to this customer via invoice
|
||||
$invoice = \App\Models\Invoice::where('customer_id', $user->id)
|
||||
->where('container_id', $order_id)
|
||||
->with('container')
|
||||
->first();
|
||||
|
||||
if (!$invoice || !$invoice->container) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Order not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$container = $invoice->container;
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'track' => [
|
||||
'order_id' => $order->order_id,
|
||||
'shipment_status' => $shipment->status ?? 'pending',
|
||||
'shipment_date' => $shipment->shipment_date ?? null,
|
||||
'order_id' => $container->id,
|
||||
'container_number' => $container->container_number,
|
||||
'status' => $container->status,
|
||||
'container_date' => $container->container_date,
|
||||
]
|
||||
]);
|
||||
}
|
||||
@@ -289,6 +346,44 @@ public function invoiceDetails($invoice_id)
|
||||
]);
|
||||
}
|
||||
|
||||
// public function confirmOrder($order_id)
|
||||
// {
|
||||
// $user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
// if (! $user) {
|
||||
// return response()->json([
|
||||
// 'success' => false,
|
||||
// 'message' => 'Unauthorized'
|
||||
// ], 401);
|
||||
// }
|
||||
|
||||
// $order = $user->orders()
|
||||
// ->where('order_id', $order_id)
|
||||
// ->first();
|
||||
|
||||
// if (! $order) {
|
||||
// return response()->json([
|
||||
// 'success' => false,
|
||||
// 'message' => 'Order not found'
|
||||
// ], 404);
|
||||
// }
|
||||
|
||||
// // 🚫 Only allow confirm from order_placed
|
||||
// if ($order->status !== 'order_placed') {
|
||||
// return response()->json([
|
||||
// 'success' => false,
|
||||
// 'message' => 'Order cannot be confirmed'
|
||||
// ], 422);
|
||||
// }
|
||||
|
||||
// $order->status = 'order_confirmed';
|
||||
// $order->save();
|
||||
|
||||
// return response()->json([
|
||||
// 'success' => true,
|
||||
// 'message' => 'Order confirmed successfully'
|
||||
// ]);
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
26
app/Imports/OrderItemsPreviewImport.php
Normal file
26
app/Imports/OrderItemsPreviewImport.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace App\Imports;
|
||||
|
||||
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class OrderItemsPreviewImport implements ToCollection
|
||||
{
|
||||
public array $rows = [];
|
||||
|
||||
public function collection(Collection $collection)
|
||||
{
|
||||
$header = $collection->first()->map(fn ($h) => strtolower(trim($h)))->toArray();
|
||||
|
||||
foreach ($collection->skip(1) as $row) {
|
||||
$item = [];
|
||||
foreach ($header as $i => $key) {
|
||||
$item[$key] = $row[$i] ?? null;
|
||||
}
|
||||
|
||||
if (!empty($item['description'])) {
|
||||
$this->rows[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,6 @@ class ChatMessage extends Model
|
||||
'read_by_user',
|
||||
'client_id',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* The ticket this message belongs to.
|
||||
*/
|
||||
|
||||
30
app/Models/Container.php
Normal file
30
app/Models/Container.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Container extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'container_name',
|
||||
'container_number',
|
||||
'container_date',
|
||||
'status',
|
||||
'excel_file',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'container_date' => 'date',
|
||||
];
|
||||
|
||||
public function rows()
|
||||
{
|
||||
return $this->hasMany(ContainerRow::class);
|
||||
}
|
||||
|
||||
public function invoices()
|
||||
{
|
||||
return $this->hasMany(Invoice::class);
|
||||
}
|
||||
}
|
||||
23
app/Models/ContainerRow.php
Normal file
23
app/Models/ContainerRow.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ContainerRow extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'container_id',
|
||||
'row_index',
|
||||
'data',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'data' => 'array',
|
||||
];
|
||||
|
||||
public function container()
|
||||
{
|
||||
return $this->belongsTo(Container::class);
|
||||
}
|
||||
}
|
||||
@@ -9,41 +9,36 @@ class Invoice extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'order_id',
|
||||
'customer_id',
|
||||
'mark_no',
|
||||
|
||||
'invoice_number',
|
||||
'invoice_date',
|
||||
'due_date',
|
||||
|
||||
'payment_method',
|
||||
'reference_no',
|
||||
'status',
|
||||
|
||||
'final_amount', // without tax
|
||||
|
||||
'tax_type', // gst / igst
|
||||
'gst_percent', // only used for gst UI input
|
||||
'cgst_percent',
|
||||
'sgst_percent',
|
||||
'igst_percent',
|
||||
|
||||
'gst_amount', // total tax amount
|
||||
'final_amount_with_gst',
|
||||
|
||||
'customer_name',
|
||||
'company_name',
|
||||
'customer_email',
|
||||
'customer_mobile',
|
||||
'customer_address',
|
||||
'pincode',
|
||||
|
||||
'pdf_path',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'container_id',
|
||||
'customer_id',
|
||||
'mark_no',
|
||||
'invoice_number',
|
||||
'invoice_date',
|
||||
'due_date',
|
||||
'payment_method',
|
||||
'reference_no',
|
||||
'status',
|
||||
'final_amount',
|
||||
'gst_percent',
|
||||
'gst_amount',
|
||||
'final_amount_with_gst',
|
||||
'customer_name',
|
||||
'company_name',
|
||||
'customer_email',
|
||||
'customer_mobile',
|
||||
'customer_address',
|
||||
'pincode',
|
||||
'pdf_path',
|
||||
'notes',
|
||||
// totals from charge groups
|
||||
'charge_groups_total',
|
||||
'grand_total_with_charges',
|
||||
'tax_type',
|
||||
'cgst_percent',
|
||||
'sgst_percent',
|
||||
'igst_percent',
|
||||
];
|
||||
|
||||
/****************************
|
||||
* Relationships
|
||||
@@ -54,29 +49,38 @@ class Invoice extends Model
|
||||
return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC');
|
||||
}
|
||||
|
||||
public function order()
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
}
|
||||
// public function container()
|
||||
// {
|
||||
// return $this->belongsTo(Container::class);
|
||||
// }
|
||||
|
||||
public function customer()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'customer_id');
|
||||
}
|
||||
|
||||
public function installments()
|
||||
{
|
||||
return $this->hasMany(InvoiceInstallment::class);
|
||||
}
|
||||
|
||||
public function chargeGroups()
|
||||
{
|
||||
return $this->hasMany(InvoiceChargeGroup::class, 'invoice_id');
|
||||
}
|
||||
|
||||
/****************************
|
||||
* Helper Functions
|
||||
****************************/
|
||||
|
||||
// Auto calculate GST fields (you can call this in controller before saving)
|
||||
// (Items based calculateTotals वापरणार नाहीस तरी ठेवू शकतोस)
|
||||
public function calculateTotals()
|
||||
{
|
||||
$gst = ($this->final_amount * $this->gst_percent) / 100;
|
||||
$this->gst_amount = $gst;
|
||||
$this->gst_amount = $gst;
|
||||
$this->final_amount_with_gst = $this->final_amount + $gst;
|
||||
}
|
||||
|
||||
// Check overdue status condition
|
||||
public function isOverdue()
|
||||
{
|
||||
return $this->status === 'pending' && now()->gt($this->due_date);
|
||||
@@ -84,28 +88,46 @@ class Invoice extends Model
|
||||
|
||||
public function getShipment()
|
||||
{
|
||||
return $this->order?->shipments?->first();
|
||||
return null;
|
||||
}
|
||||
|
||||
public function installments()
|
||||
// ✅ Charge groups base total (WITHOUT GST)
|
||||
public function getChargeGroupsTotalAttribute()
|
||||
{
|
||||
// base = total_charge sum
|
||||
return (float) $this->chargeGroups->sum('total_charge');
|
||||
}
|
||||
|
||||
// ✅ Grand total: Charge groups base + GST (items ignore)
|
||||
public function getGrandTotalWithChargesAttribute()
|
||||
{
|
||||
$base = (float) ($this->charge_groups_total ?? 0);
|
||||
$gst = (float) ($this->gst_amount ?? 0);
|
||||
|
||||
return $base + $gst;
|
||||
}
|
||||
|
||||
public function totalPaid(): float
|
||||
{
|
||||
return (float) $this->installments()->sum('amount');
|
||||
}
|
||||
|
||||
public function remainingAmount(): float
|
||||
{
|
||||
$grand = (float) $this->grand_total_with_charges;
|
||||
$paid = (float) $this->totalPaid();
|
||||
|
||||
return max(0, $grand - $paid);
|
||||
}
|
||||
|
||||
public function isLockedForEdit(): bool
|
||||
{
|
||||
return $this->status === 'paid';
|
||||
}
|
||||
|
||||
public function container()
|
||||
{
|
||||
return $this->hasMany(InvoiceInstallment::class);
|
||||
return $this->belongsTo(\App\Models\Container::class, 'container_id');
|
||||
}
|
||||
|
||||
// App\Models\Invoice.php
|
||||
|
||||
public function totalPaid()
|
||||
{
|
||||
return $this->installments()->sum('amount');
|
||||
}
|
||||
|
||||
public function remainingAmount()
|
||||
{
|
||||
return max(
|
||||
($this->final_amount_with_gst ?? 0) - $this->totalPaid(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
31
app/Models/InvoiceChargeGroup.php
Normal file
31
app/Models/InvoiceChargeGroup.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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',
|
||||
|
||||
'tax_type',
|
||||
'gst_percent',
|
||||
'total_with_gst',
|
||||
];
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ class InvoiceItem extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'invoice_id',
|
||||
'container_id', // Container mapping
|
||||
'container_row_index', // Container row index
|
||||
|
||||
'description',
|
||||
'ctn',
|
||||
@@ -27,6 +29,7 @@ class InvoiceItem extends Model
|
||||
'ttl_kg',
|
||||
|
||||
'shop_no',
|
||||
'mark_no',
|
||||
];
|
||||
|
||||
/****************************
|
||||
@@ -37,4 +40,79 @@ class InvoiceItem extends Model
|
||||
{
|
||||
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,11 +58,31 @@ class Order extends Model
|
||||
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');
|
||||
// }
|
||||
|
||||
|
||||
const STATUS_LABELS = [
|
||||
'order_placed' => 'Order Placed',
|
||||
'order_confirmed' => 'Order Confirmed',
|
||||
'supplier_warehouse' => 'Supplier Warehouse',
|
||||
'consolidate_warehouse'=> 'Consolidate Warehouse',
|
||||
'export_custom' => 'Export Custom',
|
||||
'international_transit'=> 'International Transit',
|
||||
'arrived_india' => 'Arrived at India',
|
||||
'import_custom' => 'Import Custom',
|
||||
'warehouse' => 'Warehouse',
|
||||
'domestic_distribution'=> 'Domestic Distribution',
|
||||
'out_for_delivery' => 'Out for Delivery',
|
||||
'delivered' => 'Delivered',
|
||||
];
|
||||
|
||||
public function getStatusLabelAttribute()
|
||||
{
|
||||
return $this->hasOne(\App\Models\Invoice::class, 'order_id', 'id');
|
||||
return self::STATUS_LABELS[$this->status]
|
||||
?? ucfirst(str_replace('_', ' ', $this->status));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -45,25 +45,6 @@ class Shipment extends Model
|
||||
return $this->belongsToMany(Order::class, 'shipment_items', 'shipment_id', 'order_id');
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// STATUS CONSTANTS
|
||||
// ---------------------------
|
||||
|
||||
const STATUS_PENDING = 'pending';
|
||||
const STATUS_IN_TRANSIT = 'in_transit';
|
||||
const STATUS_DISPATCHED = 'dispatched';
|
||||
const STATUS_DELIVERED = 'delivered';
|
||||
|
||||
public static function statusOptions()
|
||||
{
|
||||
return [
|
||||
self::STATUS_PENDING => 'Pending',
|
||||
self::STATUS_IN_TRANSIT => 'In Transit',
|
||||
self::STATUS_DISPATCHED => 'Dispatched',
|
||||
self::STATUS_DELIVERED => 'Delivered',
|
||||
];
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// HELPERS
|
||||
// ---------------------------
|
||||
@@ -73,8 +54,38 @@ class Shipment extends Model
|
||||
return $this->items()->count();
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// STATUS CONSTANTS (LOGISTICS FLOW)
|
||||
// ---------------------------
|
||||
const STATUS_SHIPMENT_READY = 'shipment_ready';
|
||||
const STATUS_EXPORT_CUSTOM = 'export_custom';
|
||||
const STATUS_INTERNATIONAL_TRANSIT= 'international_transit';
|
||||
const STATUS_ARRIVED_INDIA = 'arrived_india';
|
||||
const STATUS_IMPORT_CUSTOM = 'import_custom';
|
||||
const STATUS_WAREHOUSE = 'warehouse';
|
||||
const STATUS_DOMESTIC_DISTRIBUTION= 'domestic_distribution';
|
||||
const STATUS_OUT_FOR_DELIVERY = 'out_for_delivery';
|
||||
const STATUS_DELIVERED = 'delivered';
|
||||
|
||||
public static function statusOptions()
|
||||
{
|
||||
return [
|
||||
self::STATUS_SHIPMENT_READY => 'Shipment Ready',
|
||||
self::STATUS_EXPORT_CUSTOM => 'Export Custom',
|
||||
self::STATUS_INTERNATIONAL_TRANSIT => 'International Transit',
|
||||
self::STATUS_ARRIVED_INDIA => 'Arrived at India',
|
||||
self::STATUS_IMPORT_CUSTOM => 'Import Custom',
|
||||
self::STATUS_WAREHOUSE => 'Warehouse',
|
||||
self::STATUS_DOMESTIC_DISTRIBUTION => 'Domestic Distribution',
|
||||
self::STATUS_OUT_FOR_DELIVERY => 'Out for Delivery',
|
||||
self::STATUS_DELIVERED => 'Delivered',
|
||||
];
|
||||
}
|
||||
|
||||
public function statusLabel()
|
||||
{
|
||||
return self::statusOptions()[$this->status] ?? ucfirst($this->status);
|
||||
return self::statusOptions()[$this->status]
|
||||
?? ucfirst(str_replace('_', ' ', $this->status));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -89,10 +89,8 @@ class User extends Authenticatable implements JWTSubject
|
||||
{
|
||||
return [];
|
||||
}
|
||||
public function invoices()
|
||||
{
|
||||
return $this->hasMany(\App\Models\Invoice::class, 'customer_id', 'id');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// App\Models\User.php
|
||||
|
||||
@@ -107,6 +105,10 @@ public function invoiceInstallments()
|
||||
);
|
||||
}
|
||||
|
||||
public function invoices()
|
||||
{
|
||||
return $this->hasMany(\App\Models\Invoice::class, 'customer_id', 'id');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ class CreateInvoiceItemsTable extends Migration
|
||||
$table->string('shop_no')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unsignedBigInteger('container_id')->nullable()->after('invoice_id');
|
||||
$table->integer('container_row_index')->nullable()->after('container_id');
|
||||
|
||||
// FK
|
||||
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');
|
||||
@@ -49,4 +52,6 @@ class CreateInvoiceItemsTable extends Migration
|
||||
});
|
||||
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', 21)
|
||||
->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']);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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) {
|
||||
// column आधीच आहे का हे check करून, नसेल तरच add करायचा
|
||||
if (!Schema::hasColumn('invoices', 'due_date')) {
|
||||
$table->date('due_date')
|
||||
->nullable()
|
||||
->after('invoice_date');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('invoices', 'due_date')) {
|
||||
$table->dropColumn('due_date');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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::table('containers', function (Blueprint $table) {
|
||||
$table->string('status', 50)->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('containers', function (Blueprint $table) {
|
||||
$table->string('status', 20)->change();
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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('invoice_charge_groups', function (Blueprint $table) {
|
||||
$table->string('tax_type')->nullable()->after('total_charge');
|
||||
$table->decimal('gst_percent', 5, 2)->default(0)->after('tax_type');
|
||||
$table->decimal('total_with_gst', 15, 2)->default(0)->after('gst_percent');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('invoice_charge_groups', function (Blueprint $table) {
|
||||
$table->dropColumn(['tax_type', 'gst_percent', 'total_with_gst']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddChargeColumnsToInvoicesTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('invoices', 'charge_groups_total')) {
|
||||
$table->decimal('charge_groups_total', 15, 2)
|
||||
->nullable()
|
||||
->after('final_amount_with_gst');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('invoices', 'grand_total_with_charges')) {
|
||||
$table->decimal('grand_total_with_charges', 15, 2)
|
||||
->nullable()
|
||||
->after('charge_groups_total');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('invoices', 'charge_groups_total')) {
|
||||
$table->dropColumn('charge_groups_total');
|
||||
}
|
||||
if (Schema::hasColumn('invoices', 'grand_total_with_charges')) {
|
||||
$table->dropColumn('grand_total_with_charges');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
DB::statement("
|
||||
ALTER TABLE `invoices`
|
||||
MODIFY `status` ENUM('pending','paying','paid','overdue')
|
||||
NOT NULL DEFAULT 'pending'
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::statement("
|
||||
ALTER TABLE `invoices`
|
||||
MODIFY `status` ENUM('pending','paid','overdue')
|
||||
NOT NULL DEFAULT 'pending'
|
||||
");
|
||||
}
|
||||
};
|
||||
@@ -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->string('mark_no')->nullable(); // after() काहीही नको
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('invoice_items', function (Blueprint $table) {
|
||||
$table->dropColumn('mark_no');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
@@ -25,6 +25,12 @@ class PermissionSeeder extends Seeder
|
||||
// EXTRA (ORDERS)
|
||||
'orders.view', // you added this separately
|
||||
|
||||
// CONTAINER
|
||||
'container.view',
|
||||
'container.create',
|
||||
'container.update',
|
||||
'container.delete',
|
||||
|
||||
// SHIPMENT
|
||||
'shipment.view',
|
||||
'shipment.create',
|
||||
|
||||
BIN
public/images/kentlogo1.png
Normal file
BIN
public/images/kentlogo1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000031.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000031.pdf
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000044.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000044.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000046.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000046.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000048.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000048.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000051.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000051.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000007.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000007.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000012.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000012.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000013.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000013.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000014.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000014.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000015.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000015.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000110.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000110.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000116.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000116.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000117.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000117.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000134.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000134.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000183.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000183.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000184.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000184.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000185.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000185.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000202.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000202.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000205.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000205.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000206.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000206.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000209.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000209.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000213.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000213.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000214.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000214.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000217.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000217.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000218.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000218.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000220.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000220.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000221.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000221.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000222.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000222.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000225.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000225.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000253.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000253.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000256.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000256.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000257.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000257.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000258.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000258.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000259.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000259.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000260.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000260.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000261.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000261.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000262.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000262.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000263.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000263.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000264.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000264.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000265.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000265.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,102 @@
|
||||
<div class="container-fluid py-2">
|
||||
|
||||
{{-- Top info cards (container / date / status) --}}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Container Name</small>
|
||||
<div class="fw-semibold">{{ $container->container_name ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Container No</small>
|
||||
<div class="fw-semibold">{{ $container->container_number ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Container Date</small>
|
||||
<div class="fw-semibold">
|
||||
{{ $container->container_date ? \Carbon\Carbon::parse($container->container_date)->format('d-m-Y') : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Status</small>
|
||||
<div class="fw-semibold text-capitalize">{{ $container->status ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Totals (CTN / Qty / CBM / KG) --}}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Total CTN</small>
|
||||
<div class="fw-semibold">{{ $summary['total_ctn'] ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Total Qty</small>
|
||||
<div class="fw-semibold">{{ $summary['total_qty'] ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Total CBM</small>
|
||||
<div class="fw-semibold">{{ $summary['total_cbm'] ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Total KG</small>
|
||||
<div class="fw-semibold">{{ $summary['total_kg'] ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Excel rows – same headings as container_show --}}
|
||||
@php
|
||||
$allHeadings = [];
|
||||
foreach ($container->rows as $row) {
|
||||
if (is_array($row->data)) {
|
||||
$allHeadings = array_unique(array_merge($allHeadings, array_keys($row->data)));
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="table-responsive" style="max-height: 500px; border-radius: 8px;">
|
||||
<table class="table table-sm table-bordered align-middle">
|
||||
<thead class="table-warning">
|
||||
<tr>
|
||||
<th style="width: 40px;">#</th>
|
||||
@foreach($allHeadings as $heading)
|
||||
<th>{{ $heading }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($container->rows as $index => $row)
|
||||
<tr>
|
||||
<td>{{ $index + 1 }}</td>
|
||||
@foreach($allHeadings as $heading)
|
||||
@php
|
||||
$val = is_array($row->data) ? ($row->data[$heading] ?? '') : '';
|
||||
@endphp
|
||||
<td>{{ $val }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ count($allHeadings) + 1 }}" class="text-center text-muted py-3">
|
||||
No Excel rows for this container.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -6,6 +6,7 @@
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<style>
|
||||
/* ---------- Base ---------- */
|
||||
|
||||
:root{
|
||||
--primary-1:#1a2951;
|
||||
--primary-2:#243a72;
|
||||
@@ -37,7 +38,7 @@ body {
|
||||
/* header */
|
||||
.account-header {
|
||||
margin-bottom: 18px;
|
||||
background: linear-gradient(90deg,var(--primary-1),var(--primary-2));
|
||||
background:linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 22px 26px;
|
||||
border-radius: var(--rounded);
|
||||
box-shadow: 0 6px 18px rgba(34,50,90,0.12);
|
||||
@@ -48,7 +49,7 @@ body {
|
||||
|
||||
/* top actions row */
|
||||
.top-actions {
|
||||
display:flex; align-items:center; justify-content:space-between;
|
||||
align-items:center; justify-content:space-between;
|
||||
gap:12px; margin:16px 0 20px 0; flex-wrap:wrap;
|
||||
}
|
||||
.top-actions .left {
|
||||
@@ -62,12 +63,12 @@ body {
|
||||
|
||||
.btn {
|
||||
display:inline-flex; align-items:center; justify-content:center; gap:8px;
|
||||
background: linear-gradient(90deg,var(--primary-1),var(--primary-2));
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color:#fff; border:none; padding:10px 16px; border-radius:10px; font-weight:600;
|
||||
cursor:pointer; transition: transform .15s ease, box-shadow .15s;
|
||||
}
|
||||
.btn.ghost { background: transparent; color:var(--primary-1); border:1.5px solid #dbe4f5; box-shadow:none; }
|
||||
.btn:hover{ transform: translateY(-3px); box-shadow: 0 8px 26px rgba(36,58,114,0.12); }
|
||||
.btn:hover{ transform: translateY(-3px); box-shadow: 0 8px 26px rgba(227, 229, 234, 0.12); }
|
||||
|
||||
/* account panels */
|
||||
.account-panels {
|
||||
@@ -89,12 +90,12 @@ body {
|
||||
background: var(--card-bg);
|
||||
border-radius:12px;
|
||||
box-shadow:0 8px 20px rgba(25,40,80,0.06);
|
||||
padding:22px;
|
||||
padding:20px; /* 005 */
|
||||
box-sizing:border-box;
|
||||
overflow-x:auto;
|
||||
transition: transform .12s, box-shadow .12s;
|
||||
min-height: 520px;
|
||||
display: flex;
|
||||
/* display: flex; */ /* 005 */
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
@@ -205,8 +206,8 @@ tr:hover td{ background:#fbfdff; }
|
||||
.toggle-switch-btn {
|
||||
appearance:none;
|
||||
-webkit-appearance:none;
|
||||
width:60px;
|
||||
height:24px;
|
||||
width:64px;
|
||||
height:26.5px; /* 005 */
|
||||
background:#f25b5b;
|
||||
border:2px solid #f25b5b;
|
||||
border-radius:999px;
|
||||
@@ -313,7 +314,7 @@ tr:hover td{ background:#fbfdff; }
|
||||
margin-top: 15px;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid #eef3fb;
|
||||
margin-right:550px;
|
||||
/* margin-right:550px; */
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
@@ -326,14 +327,15 @@ tr:hover td{ background:#fbfdff; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-right:-1050px;
|
||||
position: absolute;
|
||||
right: 16px; /* 005 */
|
||||
|
||||
}
|
||||
.pagination-controls1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-right:-550px;
|
||||
margin-right:-550px;
|
||||
|
||||
}
|
||||
|
||||
@@ -341,7 +343,7 @@ tr:hover td{ background:#fbfdff; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-right:-550px;
|
||||
margin-right:8=500px;
|
||||
|
||||
}
|
||||
|
||||
@@ -457,7 +459,7 @@ tr:hover td{ background:#fbfdff; }
|
||||
filter: brightness(0) saturate(100%) invert(84%) sepia(8%) saturate(165%) hue-rotate(179deg) brightness(89%) contrast(86%);
|
||||
}
|
||||
|
||||
/* ---------- Entry Details Modal (existing) ---------- */
|
||||
/* ---------- Entry Details Modal (Enhanced) ---------- */
|
||||
.modal-fade1 {
|
||||
position:fixed;
|
||||
inset:0;
|
||||
@@ -470,15 +472,18 @@ tr:hover td{ background:#fbfdff; }
|
||||
}
|
||||
.modal-fade1.modal-open { display:flex; }
|
||||
.modal-box1 {
|
||||
background:#fff;
|
||||
border-radius:12px;
|
||||
padding:20px 24px;
|
||||
box-shadow:0 14px 40px rgba(18,30,60,0.12);
|
||||
max-width:1200px;
|
||||
width:100%;
|
||||
max-height:92vh;
|
||||
overflow:auto;
|
||||
min-height: 500px;
|
||||
background:#fff;
|
||||
border-radius:12px;
|
||||
padding:20px 24px;
|
||||
box-shadow:0 14px 40px rgba(18,30,60,0.12);
|
||||
max-width:1200px;
|
||||
width:100%;
|
||||
max-height:92vh;
|
||||
overflow:auto;
|
||||
min-height: 500px;
|
||||
}
|
||||
#entryOrdersModal {
|
||||
z-index: 1300;
|
||||
}
|
||||
#entryOrdersModal {
|
||||
z-index: 1300;
|
||||
@@ -488,7 +493,7 @@ tr:hover td{ background:#fbfdff; }
|
||||
.entry-summary-cards {
|
||||
display:flex;
|
||||
gap:16px;
|
||||
margin-bottom:20px;
|
||||
margin-top:25px;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
.entry-summary-card {
|
||||
@@ -503,6 +508,184 @@ tr:hover td{ background:#fbfdff; }
|
||||
.entry-summary-label{ font-size:12px; color:var(--muted); }
|
||||
.entry-summary-value{ font-size:18px; font-weight:700; color:#253047; margin-top:6px; }
|
||||
|
||||
/* Enhanced dropdown */
|
||||
.installment-status-dropdown {
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
padding: 10px 40px 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1.5px solid #e3eaf6;
|
||||
background: white;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%236b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.installment-status-dropdown:hover {
|
||||
border-color: #c2d1f0;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.installment-status-dropdown:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(39, 109, 234, 0.1);
|
||||
}
|
||||
|
||||
/* Status-specific dropdown options */
|
||||
.installment-status-dropdown option {
|
||||
padding: 12px !important;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.installment-status-dropdown option[value="Pending"] {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.installment-status-dropdown option[value="Loading"] {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.installment-status-dropdown option[value="Packed"] {
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.installment-status-dropdown option[value="Dispatched"] {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.installment-status-dropdown option[value="Delivered"] {
|
||||
color: #0c6b2e;
|
||||
}
|
||||
|
||||
/* ---------- Entry Orders Modal (Enhanced) ---------- */
|
||||
.entry-orders-modal .modal-box1 {
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(18, 30, 60, 0.25);
|
||||
}
|
||||
|
||||
.entry-orders-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 24px 30px;
|
||||
color: white;
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
.entry-orders-header h2 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.entry-orders-header .subtitle {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.entry-orders-content {
|
||||
padding: 30px;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Orders table in modal */
|
||||
.orders-table-container {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #eef3fb;
|
||||
border-radius: 12px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.orders-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.orders-table th {
|
||||
background: linear-gradient(90deg, #f8fbff, #f5f9ff);
|
||||
padding: 14px 16px;
|
||||
text-align: left;
|
||||
font-weight: 700;
|
||||
color: var(--primary-1);
|
||||
border-bottom: 2px solid #eef3fb;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.orders-table td {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #f1f6ff;
|
||||
}
|
||||
|
||||
.orders-table tr:hover {
|
||||
background: #fbfdff;
|
||||
}
|
||||
|
||||
.orders-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Order ID with badge style */
|
||||
.order-id-badge {
|
||||
display: inline-block;
|
||||
background: linear-gradient(90deg, #f0f7ff, #e6f0ff);
|
||||
color: var(--primary-1);
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
border: 1px solid #dbe4f5;
|
||||
}
|
||||
|
||||
/* Summary row */
|
||||
.orders-summary-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
background: linear-gradient(90deg, #e6ebf5 0%, #f9fbff 100%);
|
||||
|
||||
|
||||
border-radius: 10px;
|
||||
margin-top: 20px;
|
||||
border: 1px solid #eef3fb;
|
||||
}
|
||||
|
||||
.orders-summary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.orders-summary-value {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-1);
|
||||
}
|
||||
|
||||
.orders-summary-label {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin-top: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* installment modal */
|
||||
#installmentModal .modal-box1 { max-width:720px; min-width:380px; }
|
||||
|
||||
@@ -563,7 +746,7 @@ tr:hover td{ background:#fbfdff; }
|
||||
|
||||
/* Combined filters row styling */
|
||||
.combined-filters-row {
|
||||
display: flex;
|
||||
display: ruby; /* 005 */
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
@@ -576,8 +759,8 @@ tr:hover td{ background:#fbfdff; }
|
||||
}
|
||||
|
||||
.right{
|
||||
margin-left:auto;
|
||||
margin-top:-16px;
|
||||
/* margin-left:auto;
|
||||
margin-top:-16px; */ /* 005 */
|
||||
}
|
||||
|
||||
.filter-group1 {
|
||||
@@ -827,7 +1010,10 @@ tr:hover td{ background:#fbfdff; }
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
background: linear-gradient(90deg, #f9fbff, #f7faff);
|
||||
background:linear-gradient(90deg, #e6ebf5 0%, #f9fbff 100%);
|
||||
|
||||
|
||||
|
||||
border-radius: 8px;
|
||||
border: 1px solid #eef3fb;
|
||||
}
|
||||
@@ -953,6 +1139,15 @@ tr:hover td{ background:#fbfdff; }
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.combined-top-row .btn:hover {
|
||||
color: #ffffff !important;
|
||||
background-color: inherit !important;
|
||||
border-color: inherit !important;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
|
||||
.remove-order-btn:hover {
|
||||
background: #d42c3f;
|
||||
}
|
||||
@@ -1100,6 +1295,40 @@ tr:hover td{ background:#fbfdff; }
|
||||
.pagination-container {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
/* Responsive modals */
|
||||
|
||||
|
||||
.entry-details-modal .modal-box1,
|
||||
.entry-orders-modal .modal-box1 {
|
||||
margin: 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.entry-details-header,
|
||||
.entry-orders-header {
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.entry-details-content,
|
||||
.entry-orders-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.entry-summary-cards {
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.entry-summary-value {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.orders-summary-row {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Zoom out specific fix */
|
||||
@@ -1136,6 +1365,10 @@ tr:hover td{ background:#fbfdff; }
|
||||
.pagination-info {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.entry-summary-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Prevent horizontal scroll on very small screens */
|
||||
@@ -1168,6 +1401,7 @@ html, body {
|
||||
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<div class="account-container">
|
||||
@@ -1313,8 +1547,8 @@ html, body {
|
||||
<!-- CREATE ORDER POPUP MODAL -->
|
||||
<div class="create-order-modal" id="createOrderModal">
|
||||
<div class="modal-box">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:16px;">
|
||||
<div style="font-size:20px; font-weight:800; color:var(--primary-1)">Create New Installment</div>
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:16px; background:linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding:20px; border-radius:12px; color:white; margin-top:-20px; margin-left:-20px; margin-right:-15px; margin-top:-15px;">
|
||||
<div style="font-size:20px; font-weight:800;">Create New Installment</div>
|
||||
<button class="btn ghost" id="closeCreateModal" title="Close create form">✕</button>
|
||||
</div>
|
||||
|
||||
@@ -1405,111 +1639,159 @@ html, body {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ENTRY DETAILS MODAL -->
|
||||
<div class="modal-fade1" id="entryDetailsModal">
|
||||
<div class="modal-box1 entry-details-modal">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||
<div>
|
||||
<h2 style="margin:0;font-size:20px;color:#223256;font-weight:800">Entry Details — <span id="entryDetailsId">-</span></h2>
|
||||
<div style="font-size:13px;color:var(--muted)">Complete view of all installments for this entry.</div>
|
||||
<!-- ENTRY DETAILS MODAL (Enhanced) -->
|
||||
<div class="modal-fade1 entry-details-modal" id="entryDetailsModal">
|
||||
<div class="modal-box1">
|
||||
<div class="entry-details-header" >
|
||||
<h2 >Entry Details</h2>
|
||||
<div class="subtitle">
|
||||
<span id="entryDetailsId">Loading...</span>
|
||||
<span>• Complete view of all installments for this entry</span>
|
||||
</div>
|
||||
<div><button class="btn ghost" onclick="closeEntryDetailsModal()">Close</button></div>
|
||||
</div>
|
||||
|
||||
<div class="entry-details-content">
|
||||
<div class="entry-summary-cards">
|
||||
<div class="entry-summary-card">
|
||||
<div class="entry-summary-label">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
|
||||
</svg>
|
||||
Original Amount
|
||||
</div>
|
||||
<div class="entry-summary-value" id="originalAmount">-</div>
|
||||
</div>
|
||||
<div class="entry-summary-card">
|
||||
<div class="entry-summary-label">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
|
||||
</svg>
|
||||
Total Processed
|
||||
</div>
|
||||
<div class="entry-summary-value" id="totalProcessed">-</div>
|
||||
</div>
|
||||
<div class="entry-summary-card">
|
||||
<div class="entry-summary-label">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>
|
||||
</svg>
|
||||
Pending Balance
|
||||
</div>
|
||||
<div class="entry-summary-value" id="pendingBalance">-</div>
|
||||
</div>
|
||||
<div class="entry-summary-card">
|
||||
<div class="entry-summary-label">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
Total Installments
|
||||
</div>
|
||||
<div class="entry-summary-value" id="totalInstallments">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="entry-summary-cards">
|
||||
<div class="entry-summary-card">
|
||||
<div class="entry-summary-label">Original Amount</div>
|
||||
<div class="entry-summary-value" id="originalAmount">-</div>
|
||||
</div>
|
||||
<div class="entry-summary-card">
|
||||
<div class="entry-summary-label">Total Processed</div>
|
||||
<div class="entry-summary-value" id="totalProcessed">-</div>
|
||||
</div>
|
||||
<div class="entry-summary-card">
|
||||
<div class="entry-summary-label">Pending Balance</div>
|
||||
<div class="entry-summary-value" id="pendingBalance">-</div>
|
||||
</div>
|
||||
<div class="entry-summary-card">
|
||||
<div class="entry-summary-label">Total Installments</div>
|
||||
<div class="entry-summary-value" id="totalInstallments">-</div>
|
||||
<div style="margin-top: 30px;">
|
||||
<h3 style="font-size: 18px; font-weight: 700; color: var(--primary-1); margin-bottom: 16px;">
|
||||
Installment History
|
||||
</h3>
|
||||
|
||||
<div class="orders-table-container">
|
||||
<table class="entry-installments-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Installment</th>
|
||||
<th>Date</th>
|
||||
<th>Description</th>
|
||||
<th>Region</th>
|
||||
<th>Amount</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="installmentsTableBody">
|
||||
<tr><td colspan="6" class="empty-state">No installments yet</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="entry-installments-table" style="width:100%; border-collapse:collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Installment</th>
|
||||
<th>Date</th>
|
||||
<th>Description</th>
|
||||
<th>Region</th>
|
||||
<th>Amount</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="installmentsTableBody">
|
||||
<tr><td colspan="6" class="empty-state">No installments yet</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div style="display:flex; justify-content: flex-end; gap:12px; margin-top:16px;">
|
||||
<button type="button" class="btn ghost" onclick="closeEntryDetailsModal()">Close</button>
|
||||
<div style="padding: 20px 30px; border-top: 1px solid #eef3fb; display: flex; justify-content: space-between; align-items: center;">
|
||||
<button type="button" class="btn ghost" onclick="closeEntryDetailsModal()">
|
||||
Close
|
||||
</button>
|
||||
@can('account.add_installment')
|
||||
<button type="button" class="btn" id="addInstallmentFromDetails">+ Add New Installment</button>
|
||||
<button type="button" class="btn" id="addInstallmentFromDetails">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 6px;">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
Add New Installment
|
||||
</button>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ENTRY ORDERS MODAL -->
|
||||
<div class="modal-fade1" id="entryOrdersModal">
|
||||
<div class="modal-box1" style="max-width: 1000px;">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
|
||||
<div>
|
||||
<h2 style="margin:0;font-size:20px;color:#223256;font-weight:800;">
|
||||
Entry Orders
|
||||
<span id="entryOrdersEntryNo-span" style="font-size:14px;color:#6b7280;margin-left:8px;"></span>
|
||||
</h2>
|
||||
<div style="font-size:13px;color:#6f7b8f;">
|
||||
All orders associated with this entry.
|
||||
<!-- ENTRY ORDERS MODAL (Enhanced) -->
|
||||
<div class="modal-fade1 entry-orders-modal" id="entryOrdersModal">
|
||||
<div class="modal-box1">
|
||||
<div class="entry-orders-header">
|
||||
<h2>Entry Orders</h2>
|
||||
<div class="subtitle">
|
||||
<span id="entryOrdersEntryNo-span">Loading...</span>
|
||||
<span>• All orders associated with this entry</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="entry-orders-content">
|
||||
<div class="orders-table-container">
|
||||
<table class="orders-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order ID</th>
|
||||
<th>Mark No</th>
|
||||
<th>Origin</th>
|
||||
<th>Destination</th>
|
||||
<th>CTN</th>
|
||||
<th>QTY</th>
|
||||
<th>Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="entryOrdersTableBody">
|
||||
<tr>
|
||||
<td colspan="7" class="empty-state">Loading orders...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="orders-summary-row">
|
||||
<div class="orders-summary-item">
|
||||
<div class="orders-summary-value" id="ordersTotalCount">0</div>
|
||||
<div class="orders-summary-label">Total Orders</div>
|
||||
</div>
|
||||
<div class="orders-summary-item">
|
||||
<div class="orders-summary-value" id="ordersTotalQuantity">0</div>
|
||||
<div class="orders-summary-label">Total Quantity</div>
|
||||
</div>
|
||||
<div class="orders-summary-item">
|
||||
<div class="orders-summary-value" id="ordersTotalAmount">₹0</div>
|
||||
<div class="orders-summary-label">Total Amount</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn ghost" type="button" onclick="closeEntryOrdersModal()">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="orders-table-container">
|
||||
<table class="orders-table" style="width:100%;border-collapse:collapse;font-size:13px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order ID</th>
|
||||
<th>Mark No</th>
|
||||
<th>Origin</th>
|
||||
<th>Destination</th>
|
||||
<th>CTN</th>
|
||||
<th>QTY</th>
|
||||
<th>Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="entryOrdersTableBody">
|
||||
<tr>
|
||||
<td colspan="7" class="empty-state">No orders associated with this entry</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Installment Modal -->
|
||||
<div class="modal-fade1" id="installmentModal">
|
||||
<div class="modal-box1" style="max-width:720px;">
|
||||
<div style="display:flex;align-items:center; justify-content:space-between; margin-bottom:12px;">
|
||||
<div style="display:flex;align-items:center; justify-content:space-between; margin-bottom:12px; background:linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding:16px; border-radius:8px; color:white; margin-top:-15px; margin-left:-20px; margin-right:-15px;">
|
||||
<div style="font-size:18px;font-weight:800;color:#243a72;">+ Add New Installment</div>
|
||||
<button class="btn ghost" onclick="closeInstallmentModal()">✕</button>
|
||||
</div>
|
||||
@@ -2321,11 +2603,12 @@ function renderPaymentTable(list){
|
||||
<td>${escapeHtml(entry.entry_date)}</td>
|
||||
<td>${escapeHtml(entry.description)}</td>
|
||||
|
||||
<!-- Order Quantity - Clickable number without box -->
|
||||
<td>
|
||||
<button type="button" class="entry-link"
|
||||
onclick="openEntryOrdersModal('${escapeHtml(entry.entry_no)}')">
|
||||
<span onclick="openEntryOrdersModal('${escapeHtml(entry.entry_no)}')"
|
||||
style="cursor: pointer; color: #276dea; font-weight: 600; text-decoration: underline; text-decoration-color: #ccc;">
|
||||
${entry.order_quantity ?? '-'}
|
||||
</button>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>${escapeHtml(entry.region)}</td>
|
||||
@@ -2398,7 +2681,6 @@ function renderPaymentTable(list){
|
||||
|
||||
|
||||
|
||||
|
||||
function cycleToggle(btn) {
|
||||
// वर्तमान position घेऊन पुढचा स्टेट कॅल्क्युलेट करा
|
||||
let pos = parseInt(btn.dataset.pos || '0', 10); // 0 = unpaid, 1 = pending, 2 = paid
|
||||
@@ -2775,10 +3057,15 @@ async function submitEditEntry(e) {
|
||||
|
||||
|
||||
function openEntryOrdersModal(entryNo) {
|
||||
// header la entry no show kar
|
||||
// Set entry number in header
|
||||
document.getElementById('entryOrdersEntryNo-span').textContent = `(${entryNo})`;
|
||||
|
||||
// table clean / loading state
|
||||
// Reset summary values
|
||||
document.getElementById('ordersTotalCount').textContent = '0';
|
||||
document.getElementById('ordersTotalQuantity').textContent = '0';
|
||||
document.getElementById('ordersTotalAmount').textContent = '₹0';
|
||||
|
||||
// table loading state
|
||||
const tbody = document.getElementById('entryOrdersTableBody');
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
@@ -2811,36 +3098,44 @@ function openEntryOrdersModal(entryNo) {
|
||||
}
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
let totalQuantity = 0;
|
||||
let totalAmount = 0;
|
||||
|
||||
orders.forEach(order => {
|
||||
const tr = document.createElement('tr');
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
const idString = (order.orderid ?? order.id ?? '').toString().trim();
|
||||
const numericId = parseInt(idString, 10);
|
||||
const formattedId = isNaN(numericId)
|
||||
? escapeHtml(idString)
|
||||
: 'KNT-25-' + String(numericId).padStart(8, '0');
|
||||
const idString = (order.orderid ?? order.id ?? '').toString().trim();
|
||||
const numericId = parseInt(idString, 10);
|
||||
const formattedId = isNaN(numericId)
|
||||
? escapeHtml(idString)
|
||||
: 'KNT-25-' + String(numericId).padStart(8, '0');
|
||||
|
||||
// इथे वेगवेगळी शक्य keys try कर
|
||||
const amountValue =
|
||||
order.ttl_amount ??
|
||||
order.ttlamount ??
|
||||
order.total_amount ??
|
||||
order.order_amount ??
|
||||
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 = `
|
||||
<td>${formattedId}</td>
|
||||
<td>${escapeHtml(order.markno ?? order.mark_no ?? '')}</td>
|
||||
<td>${escapeHtml(order.origin ?? '')}</td>
|
||||
<td>${escapeHtml(order.destination ?? '')}</td>
|
||||
<td>${escapeHtml(order.ctn ?? '')}</td>
|
||||
<td>${escapeHtml(order.qty ?? '')}</td>
|
||||
<td>${formatCurrency(amountValue)}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<span class="order-id-badge">${formattedId}</span>
|
||||
</td>
|
||||
<td>${escapeHtml(order.markno ?? order.mark_no ?? '')}</td>
|
||||
<td>${escapeHtml(order.origin ?? '')}</td>
|
||||
<td>${escapeHtml(order.destination ?? '')}</td>
|
||||
<td>${escapeHtml(order.ctn ?? '')}</td>
|
||||
<td><strong>${quantity}</strong></td>
|
||||
<td><strong>${formatCurrency(amountValue)}</strong></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// Update summary
|
||||
document.getElementById('ordersTotalCount').textContent = orders.length;
|
||||
document.getElementById('ordersTotalQuantity').textContent = totalQuantity.toLocaleString();
|
||||
document.getElementById('ordersTotalAmount').textContent = formatCurrency(totalAmount);
|
||||
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -2988,7 +3283,7 @@ function handleSearch(){
|
||||
updatePaginationControls();
|
||||
}
|
||||
|
||||
/* ---------- Entry details & installments ---------- */
|
||||
/* ---------- Entry details & installments (Enhanced) ---------- */
|
||||
async function openEntryDetailsModal(entryNo) {
|
||||
try {
|
||||
const res = await jsonFetch('/admin/account/entry/' + encodeURIComponent(entryNo));
|
||||
@@ -2998,58 +3293,109 @@ async function openEntryDetailsModal(entryNo) {
|
||||
const entry = res.entry;
|
||||
currentEntry = entry;
|
||||
|
||||
// Set header info
|
||||
document.getElementById('entryDetailsId').textContent = entry.entry_no;
|
||||
document.getElementById('originalAmount').textContent = formatCurrency(entry.amount);
|
||||
|
||||
const totalProcessed = Number(entry.amount) - Number(entry.pending_amount);
|
||||
|
||||
// Calculate values
|
||||
const originalAmount = Number(entry.amount || 0);
|
||||
const pendingAmount = Number(entry.pending_amount || 0);
|
||||
const totalProcessed = originalAmount - pendingAmount;
|
||||
|
||||
// Update summary cards
|
||||
document.getElementById('originalAmount').textContent = formatCurrency(originalAmount);
|
||||
document.getElementById('totalProcessed').textContent = formatCurrency(totalProcessed);
|
||||
document.getElementById('pendingBalance').textContent = formatCurrency(pendingAmount);
|
||||
document.getElementById('totalInstallments').textContent = entry.installments?.length || 0;
|
||||
|
||||
document.getElementById('pendingBalance').textContent = formatCurrency(entry.pending_amount);
|
||||
document.getElementById('totalInstallments').textContent = entry.installments.length;
|
||||
|
||||
// Render installments table
|
||||
const tbody = document.getElementById('installmentsTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
entry.installments.forEach((ins, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${idx === 0 ? 'Original Entry' : 'Installment ' + idx}</td>
|
||||
<td>${escapeHtml(ins.proc_date)}</td>
|
||||
<td>${escapeHtml(ins.description)}</td>
|
||||
<td>${escapeHtml(ins.region)}</td>
|
||||
<td>${formatCurrency(ins.amount)}</td>
|
||||
|
||||
<td>
|
||||
<select class="installment-status-dropdown"
|
||||
data-id="${ins.id}"
|
||||
onchange="updateInstallmentStatus(${ins.id}, this.value)">
|
||||
<option value="Pending" ${ins.status === 'Pending' ? 'selected' : ''}
|
||||
style="color: #f59e0b; font-weight: 500; padding: 10px;">
|
||||
⏳ Pending
|
||||
</option>
|
||||
<option value="Loading" ${ins.status === 'Loading' ? 'selected' : ''}
|
||||
style="color: #3b82f6; font-weight: 500; padding: 10px;">
|
||||
📦 Loading
|
||||
</option>
|
||||
<option value="Packed" ${ins.status === 'Packed' ? 'selected' : ''}
|
||||
style="color: #8b5cf6; font-weight: 500; padding: 10px;">
|
||||
📦 Packed
|
||||
</option>
|
||||
<option value="Dispatched" ${ins.status === 'Dispatched' ? 'selected' : ''}
|
||||
style="color: #10b981; font-weight: 500; padding: 10px;">
|
||||
🚚 Dispatched
|
||||
</option>
|
||||
<option value="Delivered" ${ins.status === 'Delivered' ? 'selected' : ''}
|
||||
style="color: #0c6b2e; font-weight: 500; padding: 10px;">
|
||||
✅ Delivered
|
||||
</option>
|
||||
</select>
|
||||
|
||||
</td>
|
||||
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>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} else {
|
||||
entry.installments.forEach((ins, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// Determine installment label
|
||||
let installmentLabel = 'Original Entry';
|
||||
if (idx > 0) {
|
||||
installmentLabel = `Installment ${idx}`;
|
||||
}
|
||||
|
||||
// Status dropdown options with colors
|
||||
const statusOptions = [
|
||||
{ value: 'Pending', label: '⏳ Pending', color: '#f59e0b' },
|
||||
{ value: 'Loading', label: '📦 Loading', color: '#3b82f6' },
|
||||
{ value: 'Packed', label: '📦 Packed', color: '#8b5cf6' },
|
||||
{ value: 'Dispatched', label: '🚚 Dispatched', color: '#10b981' },
|
||||
{ value: 'Delivered', label: '✅ Delivered', color: '#0c6b2e' }
|
||||
];
|
||||
|
||||
let statusOptionsHtml = '';
|
||||
statusOptions.forEach(opt => {
|
||||
const selected = ins.status === opt.value ? 'selected' : '';
|
||||
statusOptionsHtml += `<option value="${opt.value}" ${selected} style="color: ${opt.color}; font-weight: 500;">${opt.label}</option>`;
|
||||
});
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div style="width: 32px; height: 32px; border-radius: 8px; background: linear-gradient(135deg, #f0f7ff, #e6f0ff); display: flex; align-items: center; justify-content: center; font-weight: 700; color: var(--primary-1);">
|
||||
${idx + 1}
|
||||
</div>
|
||||
<span style="font-weight: 600;">${installmentLabel}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
||||
<line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/>
|
||||
<line x1="3" y1="10" x2="21" y2="10"/>
|
||||
</svg>
|
||||
${escapeHtml(ins.proc_date || ins.date || '')}
|
||||
</div>
|
||||
</td>
|
||||
<td>${escapeHtml(ins.description || '')}</td>
|
||||
<td>
|
||||
<span style="padding: 4px 8px; background: #f0f7ff; border-radius: 6px; font-size: 12px; font-weight: 600; color: var(--primary-1);">
|
||||
${escapeHtml(ins.region || '')}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span style="font-weight: 700; color: var(--primary-1);">
|
||||
${formatCurrency(ins.amount || 0)}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<select class="installment-status-dropdown"
|
||||
data-id="${ins.id}"
|
||||
onchange="updateInstallmentStatus(${ins.id}, this.value)"
|
||||
title="Update installment status">
|
||||
${statusOptionsHtml}
|
||||
</select>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
// Show modal
|
||||
document.getElementById('entryDetailsModal').classList.add('modal-open');
|
||||
|
||||
} catch (err) {
|
||||
|
||||
855
resources/views/admin/container.blade.php
Normal file
855
resources/views/admin/container.blade.php
Normal file
@@ -0,0 +1,855 @@
|
||||
@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 DROPDOWN (badge look) */
|
||||
.status-dropdown {
|
||||
position: relative;
|
||||
min-width: 190px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status-dropdown-toggle {
|
||||
padding: 8px 16px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: #ffffff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--dark-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.status-dropdown-toggle span { white-space: nowrap; }
|
||||
|
||||
.status-dropdown-menu {
|
||||
position: absolute;
|
||||
top: -230%;
|
||||
right: 0;
|
||||
z-index: 30;
|
||||
background: #ffffff;
|
||||
border-radius: 14px;
|
||||
padding: 8px 0;
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: 1px solid var(--border-color);
|
||||
width: 220px;
|
||||
max-height: 340px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status-dropdown-menu.open { display: block; }
|
||||
|
||||
.status-option {
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
color: var(--dark-text);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: background 0.15s;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.status-option:hover { background: #eef2ff; }
|
||||
|
||||
.status-option .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
.status-option.active .dot {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
/* COLOR MAPPING per status – dropdown tint + main toggle text color */
|
||||
.status-option.status-container-ready { background: #eff6ff; color: #1d4ed8; }
|
||||
.status-option.status-export-custom { background: #fff7ed; color: #b45309; }
|
||||
.status-option.status-international-transit { background: #f5f3ff; color: #4c1d95; }
|
||||
.status-option.status-arrived-at-india { background: #ecfdf5; color: #15803d; }
|
||||
.status-option.status-import-custom { background: #fffbeb; color: #92400e; }
|
||||
.status-option.status-warehouse { background: #f4f4f5; color: #374151; }
|
||||
.status-option.status-domestic-distribution { background: #faf5ff; color: #6d28d9; }
|
||||
.status-option.status-out-for-delivery { background: #eff6ff; color: #1d4ed8; }
|
||||
.status-option.status-delivered { background: #ecfdf5; color: #15803d; }
|
||||
|
||||
.status-dropdown-toggle.status-container-ready { background: #eff6ff; color: #1d4ed8; }
|
||||
.status-dropdown-toggle.status-export-custom { background: #fff7ed; color: #b45309; }
|
||||
.status-dropdown-toggle.status-international-transit { background: #f5f3ff; color: #4c1d95; }
|
||||
.status-dropdown-toggle.status-arrived-at-india { background: #ecfdf5; color: #15803d; }
|
||||
.status-dropdown-toggle.status-import-custom { background: #fffbeb; color: #92400e; }
|
||||
.status-dropdown-toggle.status-warehouse { background: #f4f4f5; color: #374151; }
|
||||
.status-dropdown-toggle.status-domestic-distribution { background: #faf5ff; color: #6d28d9; }
|
||||
.status-dropdown-toggle.status-out-for-delivery { background: #eff6ff; color: #1d4ed8; }
|
||||
.status-dropdown-toggle.status-delivered { background: #ecfdf5; color: #15803d; }
|
||||
|
||||
.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 { position: relative; }
|
||||
|
||||
.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 {
|
||||
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 { width: 100%; }
|
||||
.status-dropdown { 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('container.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="container-ready" {{ request('status') == 'container-ready' ? 'selected' : '' }}>Container Ready</option>
|
||||
<option value="export-custom" {{ request('status') == 'export-custom' ? 'selected' : '' }}>Export Custom</option>
|
||||
<option value="international-transit" {{ request('status') == 'international-transit' ? 'selected' : '' }}>International Transit</option>
|
||||
<option value="arrived-at-india" {{ request('status') == 'arrived-at-india' ? 'selected' : '' }}>Arrived at India</option>
|
||||
<option value="import-custom" {{ request('status') == 'import-custom' ? 'selected' : '' }}>Import Custom</option>
|
||||
<option value="warehouse" {{ request('status') == 'warehouse' ? 'selected' : '' }}>Warehouse</option>
|
||||
<option value="domestic-distribution" {{ request('status') == 'domestic-distribution' ? 'selected' : '' }}>Domestic Distribution</option>
|
||||
<option value="out-for-delivery" {{ request('status') == 'out-for-delivery' ? 'selected' : '' }}>Out for Delivery</option>
|
||||
<option value="delivered" {{ request('status') == 'delivered' ? 'selected' : '' }}>Delivered</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
|
||||
@php
|
||||
$labels = [
|
||||
'container-ready' => 'Container Ready',
|
||||
'export-custom' => 'Export Custom',
|
||||
'international-transit' => 'International Transit',
|
||||
'arrived-at-india' => 'Arrived at India',
|
||||
'import-custom' => 'Import Custom',
|
||||
'warehouse' => 'Warehouse',
|
||||
'domestic-distribution' => 'Domestic Distribution',
|
||||
'out-for-delivery' => 'Out for Delivery',
|
||||
'delivered' => 'Delivered',
|
||||
];
|
||||
@endphp
|
||||
|
||||
@foreach($containers as $container)
|
||||
@php
|
||||
$status = $container->status ?? 'container-ready';
|
||||
$statusLabel = $labels[$status] ?? ucfirst(str_replace('-', ' ', $status));
|
||||
@endphp
|
||||
|
||||
<div class="container-item">
|
||||
<div class="container-header">
|
||||
<div class="container-info">
|
||||
<div class="container-avatar">
|
||||
{{ strtoupper(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">
|
||||
@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
|
||||
|
||||
@php $statusClass = 'status-' . $status; @endphp
|
||||
|
||||
<div class="status-dropdown">
|
||||
<div class="status-dropdown-toggle {{ $statusClass }}">
|
||||
<span class="status-dropdown-label">
|
||||
{{ $statusLabel }}
|
||||
</span>
|
||||
<i class="fas fa-chevron-down" style="font-size:11px;color:#4b5563;"></i>
|
||||
</div>
|
||||
<div class="status-dropdown-menu">
|
||||
@foreach($labels as $value => $label)
|
||||
@php $optClass = 'status-' . $value; @endphp
|
||||
<div class="status-option {{ $optClass }} {{ $status === $value ? 'active' : '' }}"
|
||||
data-status="{{ $value }}">
|
||||
<span class="dot"></span>
|
||||
<span>{{ $label }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endcan
|
||||
|
||||
@can('container.update')
|
||||
<a href="{{ route('containers.show', $container->id) }}" class="action-btn view-btn">
|
||||
<i class="fas fa-eye"></i> View
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
@can('container.delete')
|
||||
<form action="{{ route('containers.destroy', $container->id) }}" method="POST"
|
||||
class="delete-form"
|
||||
data-container-id="{{ $container->id }}">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="action-btn delete-btn">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</form>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 () {
|
||||
// STATUS DROPDOWN
|
||||
document.querySelectorAll('.status-dropdown').forEach(function (wrapper) {
|
||||
const toggle = wrapper.querySelector('.status-dropdown-toggle');
|
||||
const menu = wrapper.querySelector('.status-dropdown-menu');
|
||||
|
||||
toggle.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
document.querySelectorAll('.status-dropdown-menu.open').forEach(m => {
|
||||
if (m !== menu) m.classList.remove('open');
|
||||
});
|
||||
menu.classList.toggle('open');
|
||||
});
|
||||
|
||||
menu.querySelectorAll('.status-option').forEach(function (opt) {
|
||||
opt.addEventListener('click', function () {
|
||||
const status = this.dataset.status;
|
||||
const form = wrapper.closest('form');
|
||||
const labelEl = wrapper.querySelector('.status-dropdown-label');
|
||||
const toggleEl= wrapper.querySelector('.status-dropdown-toggle');
|
||||
|
||||
// UI: dropdown label + active item
|
||||
menu.querySelectorAll('.status-option').forEach(o => o.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
labelEl.textContent = this.querySelector('span:nth-child(2)').textContent;
|
||||
menu.classList.remove('open');
|
||||
|
||||
// toggle रंग class reset करून नवा status-* दे
|
||||
toggleEl.className = 'status-dropdown-toggle';
|
||||
toggleEl.classList.add('status-' + status);
|
||||
|
||||
const url = form.getAttribute('action');
|
||||
const token = form.querySelector('input[name="_token"]').value;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('_token', token);
|
||||
formData.append('status', status);
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(async res => {
|
||||
let data = null;
|
||||
try { data = await res.json(); } catch (e) {}
|
||||
if (!res.ok || !data || !data.success) {
|
||||
alert('Status update failed');
|
||||
return;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
alert('Network error while updating status');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', function () {
|
||||
document.querySelectorAll('.status-dropdown-menu.open')
|
||||
.forEach(m => m.classList.remove('open'));
|
||||
});
|
||||
|
||||
// DELETE VIA AJAX
|
||||
document.querySelectorAll('.delete-form').forEach(function (form) {
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!confirm('Are you sure you want to delete this container and all its entries?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = form.getAttribute('action');
|
||||
const token = form.querySelector('input[name="_token"]').value;
|
||||
const item = form.closest('.container-item');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('_token', token);
|
||||
formData.append('_method', 'DELETE');
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(async res => {
|
||||
let data = null;
|
||||
try { data = await res.json(); } catch (e) {}
|
||||
|
||||
if (!res.ok || !data || !data.success) {
|
||||
alert('Delete failed');
|
||||
return;
|
||||
}
|
||||
|
||||
if (item) {
|
||||
item.style.opacity = '0';
|
||||
item.style.transform = 'translateX(-10px)';
|
||||
setTimeout(() => item.remove(), 200);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
alert('Network error while deleting container');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
@endsection
|
||||
349
resources/views/admin/container_create.blade.php
Normal file
349
resources/views/admin/container_create.blade.php
Normal file
@@ -0,0 +1,349 @@
|
||||
@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>
|
||||
<ul class="small mb-0 ps-3">
|
||||
<li>Red highlighted rows indicate formula mismatches in the uploaded Excel data.</li>
|
||||
<li>Yellow highlighted rows indicate marks from the Excel file that do not match any record in the system.</li>
|
||||
</ul>
|
||||
</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 (red – formula mismatch, critical) --}}
|
||||
@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 (yellow – mark not found, warning) --}}
|
||||
@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" {{-- name fix --}}
|
||||
id="containerdate" {{-- JS साठी जुना id ठेवला --}}
|
||||
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>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const dateInput = document.getElementById('containerdate');
|
||||
if (!dateInput) return;
|
||||
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
const todayStr = `${year}-${month}-${day}`;
|
||||
|
||||
// Todays date
|
||||
dateInput.min = todayStr;
|
||||
|
||||
// old date remove
|
||||
if (dateInput.value && dateInput.value < todayStr) {
|
||||
dateInput.value = todayStr;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
251
resources/views/admin/container_pdf.blade.php
Normal file
251
resources/views/admin/container_pdf.blade.php
Normal file
@@ -0,0 +1,251 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Container {{ $container->container_number }} Summary</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: DejaVu Sans, sans-serif;
|
||||
font-size: 10px;
|
||||
margin: 10px;
|
||||
background: #e5e7ff;
|
||||
}
|
||||
|
||||
/* LOGO HEADER */
|
||||
.logo-header-wrap {
|
||||
width: 100%;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.logo-header-inner {
|
||||
display: table;
|
||||
}
|
||||
.logo-header-logo-cell {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.logo-header-text-cell {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
padding-left: 8px;
|
||||
}
|
||||
.logo-header-logo {
|
||||
height: 32px; /* banner type, कमी उंची */
|
||||
object-fit: contain;
|
||||
}
|
||||
.logo-header-title-top {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #7b1111; /* dark maroon */
|
||||
line-height: 1.1;
|
||||
}
|
||||
.logo-header-title-bottom {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #7b1111;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* COMMON CARD GRID – 4 equal columns, 2 rows */
|
||||
.card-grid {
|
||||
display: table;
|
||||
width: 100%;
|
||||
border-spacing: 8px 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.card-row {
|
||||
display: table-row;
|
||||
}
|
||||
.card {
|
||||
display: table-cell;
|
||||
width: 25%;
|
||||
padding: 7px 10px;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 12px rgba(15,35,52,0.18);
|
||||
color: #0f172a;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* INFO CARDS (FIRST ROW) */
|
||||
.info-id { background: #e0f2ff; border-left: 4px solid #2563eb; }
|
||||
.info-no { background: #dcfce7; border-left: 4px solid #22c55e; }
|
||||
.info-date { background: #fee2e2; border-left: 4px solid #ef4444; }
|
||||
.info-name { background: #fef9c3; border-left: 4px solid #f59e0b; }
|
||||
|
||||
/* TOTAL CARDS (SECOND ROW) */
|
||||
.total-ctn { background: #dbeafe; border-left: 4px solid #1d4ed8; }
|
||||
.total-qty { background: #bbf7d0; border-left: 4px solid #16a34a; }
|
||||
.total-cbm { background: #fef3c7; border-left: 4px solid #d97706; }
|
||||
.total-kg { background: #fecaca; border-left: 4px solid #dc2626; }
|
||||
|
||||
.label-text {
|
||||
font-size: 9px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
color: #4b5563;
|
||||
}
|
||||
.value-text-small {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
margin-top: 2px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.value-text-big {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* TABLE – solid yellow header */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 6px;
|
||||
table-layout: fixed;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 4px 3px;
|
||||
text-align: center;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
th {
|
||||
background: #fbd85d;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
color: #0c0909;
|
||||
}
|
||||
td {
|
||||
font-size: 9px;
|
||||
color: #111827;
|
||||
}
|
||||
tr:nth-child(even) td {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
thead { display: table-header-group; }
|
||||
tr { page-break-inside: avoid; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@php
|
||||
$totalCtn = 0;
|
||||
$totalQty = 0;
|
||||
$totalCbm = 0.0;
|
||||
$totalKg = 0.0;
|
||||
|
||||
foreach ($container->rows as $row) {
|
||||
if (!is_array($row->data)) continue;
|
||||
foreach ($row->data as $h => $v) {
|
||||
$norm = strtoupper(str_replace([' ', '/', '-', '.'],'', $h));
|
||||
$val = is_numeric(str_replace([','], '', $v)) ? floatval(str_replace([','], '', $v)) : 0;
|
||||
|
||||
if (str_contains($norm, 'TOTALCTN') || $norm === 'CTN' || str_contains($norm,'TOTALCNTR') || str_contains($norm,'TOTALCARTON')) {
|
||||
$totalCtn += $val;
|
||||
}
|
||||
if (str_contains($norm,'TOTALQTY') || str_contains($norm,'ITLQTY') || str_contains($norm,'TTLQTY')) {
|
||||
$totalQty += $val;
|
||||
}
|
||||
if (str_contains($norm,'TOTALCBM') || str_contains($norm,'TTLCBM') || str_contains($norm,'ITLCBM')) {
|
||||
$totalCbm += $val;
|
||||
}
|
||||
if (str_contains($norm,'TOTALKG') || str_contains($norm,'TTKG')) {
|
||||
$totalKg += $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$allHeadings = [];
|
||||
foreach ($container->rows as $row) {
|
||||
if (is_array($row->data)) {
|
||||
$allHeadings = array_unique(array_merge($allHeadings, array_keys($row->data)));
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
{{-- LOGO + TEXT – full left, text खाली (reference image) --}}
|
||||
<div class="logo-header-wrap">
|
||||
<div class="logo-header-inner">
|
||||
<div class="logo-header-logo-cell">
|
||||
<img src="{{ public_path('images/kentlogo1.png') }}"
|
||||
class="logo-header-logo"
|
||||
alt="Kent Logo">
|
||||
</div>
|
||||
<div class="logo-header-text-cell">
|
||||
<div class="logo-header-title-top">KENT</div>
|
||||
<div class="logo-header-title-bottom">International Pvt. Ltd.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- TWO ROW GRID – FIRST: INFO / SECOND: TOTALS --}}
|
||||
<div class="card-grid">
|
||||
<div class="card-row">
|
||||
<div class="card info-id">
|
||||
<div class="label-text">Container ID</div>
|
||||
<div class="value-text-small">{{ $container->id }}</div>
|
||||
</div>
|
||||
<div class="card info-no">
|
||||
<div class="label-text">Container Number</div>
|
||||
<div class="value-text-small">{{ $container->container_number }}</div>
|
||||
</div>
|
||||
<div class="card info-date">
|
||||
<div class="label-text">Container Date</div>
|
||||
<div class="value-text-small">
|
||||
{{ $container->container_date ? $container->container_date->format('d-m-Y') : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card info-name">
|
||||
<div class="label-text">Container Name</div>
|
||||
<div class="value-text-small">
|
||||
{{ $container->container_name ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-row">
|
||||
<div class="card total-ctn">
|
||||
<div class="label-text">Total CTN</div>
|
||||
<div class="value-text-big">{{ number_format($totalCtn, 0) }}</div>
|
||||
</div>
|
||||
<div class="card total-qty">
|
||||
<div class="label-text">Total QTY</div>
|
||||
<div class="value-text-big">{{ number_format($totalQty, 0) }}</div>
|
||||
</div>
|
||||
<div class="card total-cbm">
|
||||
<div class="label-text">Total CBM</div>
|
||||
<div class="value-text-big">{{ number_format($totalCbm, 3) }}</div>
|
||||
</div>
|
||||
<div class="card total-kg">
|
||||
<div class="label-text">Total KG</div>
|
||||
<div class="value-text-big">{{ number_format($totalKg, 2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- FULL TABLE --}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:18px;">#</th>
|
||||
@foreach($allHeadings as $heading)
|
||||
<th>{{ $heading }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($container->rows as $index => $row)
|
||||
<tr>
|
||||
<td>{{ $index + 1 }}</td>
|
||||
@foreach($allHeadings as $heading)
|
||||
<td>{{ $row->data[$heading] ?? '' }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
966
resources/views/admin/container_show.blade.php
Normal file
966
resources/views/admin/container_show.blade.php
Normal file
@@ -0,0 +1,966 @@
|
||||
@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');
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.cm-download-pdf {
|
||||
background: linear-gradient(100deg, #4c6fff 0%, #8e54e9 100%) !important;
|
||||
color: #fff !important;
|
||||
border: none !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 12.5px !important;
|
||||
padding: 8px 16px !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 14px rgba(76,111,255,0.4) !important;
|
||||
transition: all 0.2s ease !important;
|
||||
text-decoration: none !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
gap: 6px !important;
|
||||
}
|
||||
.cm-download-pdf:hover {
|
||||
transform: translateY(-1px) !important;
|
||||
box-shadow: 0 6px 20px rgba(76,111,255,0.5) !important;
|
||||
color: #fff !important;
|
||||
opacity: 0.95 !important;
|
||||
}
|
||||
|
||||
.cm-download-excel {
|
||||
background: linear-gradient(100deg, #10b981 0%, #059669 100%) !important;
|
||||
color: #fff !important;
|
||||
border: none !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 12.5px !important;
|
||||
padding: 8px 16px !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 14px rgba(16,185,129,0.4) !important;
|
||||
transition: all 0.2s ease !important;
|
||||
text-decoration: none !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
gap: 6px !important;
|
||||
}
|
||||
.cm-download-excel:hover {
|
||||
transform: translateY(-1px) !important;
|
||||
box-shadow: 0 6px 20px rgba(16,185,129,0.5) !important;
|
||||
color: #fff !important;
|
||||
opacity: 0.95 !important;
|
||||
}
|
||||
|
||||
.cm-back-btn {
|
||||
background: linear-gradient(100deg, #6b7280 0%, #4b5563 100%) !important;
|
||||
color: #fff !important;
|
||||
border: none !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 12.5px !important;
|
||||
padding: 8px 16px !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 14px rgba(107,114,128,0.4) !important;
|
||||
transition: all 0.2s ease !important;
|
||||
text-decoration: none !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
gap: 6px !important;
|
||||
}
|
||||
.cm-back-btn:hover {
|
||||
transform: translateY(-1px) !important;
|
||||
box-shadow: 0 6px 20px rgba(107,114,128,0.5) !important;
|
||||
color: #fff !important;
|
||||
opacity: 0.95 !important;
|
||||
}
|
||||
|
||||
.cm-main-card {
|
||||
border-radius: 14px;
|
||||
border: none;
|
||||
box-shadow: 0 4px 20px rgba(15,35,52,0.10);
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
.cm-info-cards-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
padding: 14px 18px 8px 18px;
|
||||
}
|
||||
.cm-info-card {
|
||||
border-radius: 16px;
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
box-shadow: 0 4px 16px rgba(15,35,52,0.12);
|
||||
border: 1px solid rgba(148,163,184,0.25);
|
||||
background: #fff;
|
||||
min-height: 70px;
|
||||
}
|
||||
.cm-info-card-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #0f172a;
|
||||
font-size: 16px;
|
||||
box-shadow: 0 3px 10px rgba(15,23,42,0.10);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cm-info-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
.cm-info-card-label {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cm-info-card-value {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cm-card-container {
|
||||
background: linear-gradient(135deg, #e0f2ff, #eef4ff);
|
||||
}
|
||||
.cm-card-container .cm-info-card-icon {
|
||||
background: linear-gradient(135deg, #2563eb, #4f46e5);
|
||||
color: #e5edff;
|
||||
}
|
||||
.cm-card-date {
|
||||
background: linear-gradient(135deg, #ecfdf3, #e0fbea);
|
||||
}
|
||||
.cm-card-date .cm-info-card-icon {
|
||||
background: linear-gradient(135deg, #16a34a, #22c55e);
|
||||
color: #ecfdf3;
|
||||
}
|
||||
.cm-card-excel {
|
||||
background: linear-gradient(135deg, #fff7ed, #fffbeb);
|
||||
}
|
||||
.cm-card-excel .cm-info-card-icon {
|
||||
background: linear-gradient(135deg, #f97316, #fb923c);
|
||||
color: #fff7ed;
|
||||
}
|
||||
|
||||
.cm-total-cards-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
padding: 4px 18px 14px 18px;
|
||||
}
|
||||
.cm-total-card {
|
||||
border-radius: 18px;
|
||||
padding: 12px 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
box-shadow: 0 8px 30px rgba(15,23,42,0.08);
|
||||
border: 1px solid rgba(148,163,184,0.25);
|
||||
}
|
||||
.cm-total-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 17px;
|
||||
flex-shrink: 0;
|
||||
color: #0f172a;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 14px rgba(15,23,42,0.15);
|
||||
}
|
||||
.cm-total-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
.cm-total-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #4b5563;
|
||||
}
|
||||
.cm-total-value {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: #111827;
|
||||
}
|
||||
.cm-total-card-ctn {
|
||||
background: linear-gradient(135deg, #e0f2ff, #eef2ff);
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
.cm-total-card-qty {
|
||||
background: linear-gradient(135deg, #dcfce7, #ecfdf5);
|
||||
border-left: 4px solid #22c55e;
|
||||
}
|
||||
.cm-total-card-cbm {
|
||||
background: linear-gradient(135deg, #fef9c3, #fffbeb);
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
.cm-total-card-kg {
|
||||
background: linear-gradient(135deg, #fee2e2, #fef2f2);
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
|
||||
.cm-filter-bar {
|
||||
padding: 4px 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);
|
||||
}
|
||||
|
||||
.cm-table-scroll-outer {
|
||||
margin: 10px 14px 30px 14px;
|
||||
border-radius: 14px;
|
||||
border: 1.5px solid #c9a359;
|
||||
box-shadow: 0 4px 14px rgba(76,111,255,0.18);
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.cm-table-wrapper {
|
||||
position: relative;
|
||||
min-width: 1200px;
|
||||
}
|
||||
|
||||
.cm-table {
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
}
|
||||
.cm-table thead tr th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
background: linear-gradient(100deg, #fde4b3 0%, #fbd48a 100%);
|
||||
color: #0c0909;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
padding: 11px 14px;
|
||||
border-bottom: 2px solid rgba(255,255,255,0.15);
|
||||
border-right: 1px solid rgba(255,255,255,0.18);
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
letter-spacing: 0.2px;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.35);
|
||||
}
|
||||
.cm-table thead tr th:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
.cm-table thead tr th:last-child {
|
||||
border-top-right-radius: 10px;
|
||||
border-right: none;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.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;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cm-table tbody tr td:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
.cm-table tbody tr:nth-child(even) td {
|
||||
background: #f8f9ff;
|
||||
}
|
||||
.cm-table tbody tr:hover td {
|
||||
background: #edf3ff !important;
|
||||
}
|
||||
.cm-row-num {
|
||||
color: #8a93a6;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
.cm-cell-readonly {
|
||||
background: #f3f4ff;
|
||||
cursor: not-allowed;
|
||||
border-color: #e2e3ff;
|
||||
}
|
||||
|
||||
.cm-save-btn-floating {
|
||||
position: fixed;
|
||||
right: 26px;
|
||||
bottom: 22px;
|
||||
z-index: 50;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border-radius: 22px;
|
||||
padding: 8px 20px;
|
||||
background: linear-gradient(90deg, #4c6fff, #8e54e9);
|
||||
border: none;
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 16px rgba(76,111,255,0.35);
|
||||
transition: opacity 0.2s, transform 0.15s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.cm-save-btn-floating:hover {
|
||||
opacity: 0.94;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.cm-save-btn-floating.cm-disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cm-toast {
|
||||
position: fixed;
|
||||
right: 26px;
|
||||
bottom: 70px;
|
||||
background: #0f172a;
|
||||
color: #f9fafb;
|
||||
font-size: 12.5px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 8px 24px rgba(15,23,42,0.25);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s, transform 0.2s;
|
||||
transform: translateY(8px);
|
||||
z-index: 60;
|
||||
}
|
||||
.cm-toast.cm-show {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.cm-empty {
|
||||
padding: 30px 20px;
|
||||
color: #8a93a6;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.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: 991px) {
|
||||
.cm-total-cards-row {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.cm-header-card .card-body {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.cm-info-cards-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.cm-table-scroll-outer {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.cm-table-wrapper {
|
||||
min-width: 900px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 575px) {
|
||||
.cm-total-cards-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container-fluid cm-wrapper">
|
||||
<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 – like Excel. TT columns auto‑calculate from CTN, QTY, CBM, KG, PRICE.
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ route('containers.index') }}" class="cm-back-btn">
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
Back to list
|
||||
</a>
|
||||
|
||||
<a href="{{ route('containers.download.pdf', $container->id) }}" class="cm-download-pdf">
|
||||
<i class="bi bi-file-earmark-pdf"></i>
|
||||
Download PDF
|
||||
</a>
|
||||
|
||||
<a href="{{ route('containers.download.excel', $container->id) }}" class="cm-download-excel">
|
||||
<i class="bi bi-file-earmark-excel"></i>
|
||||
Download Excel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card cm-main-card">
|
||||
<div class="card-header">
|
||||
<h5>Container Information</h5>
|
||||
</div>
|
||||
|
||||
<div class="cm-info-cards-row">
|
||||
<div class="cm-info-card cm-card-container">
|
||||
<div class="cm-info-card-icon">
|
||||
<i class="bi bi-box-seam"></i>
|
||||
</div>
|
||||
<div class="cm-info-card-body">
|
||||
<div class="cm-info-card-label">Container</div>
|
||||
<div class="cm-info-card-value">
|
||||
{{ $container->container_name ?? $container->container_number }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cm-info-card cm-card-date">
|
||||
<div class="cm-info-card-icon">
|
||||
<i class="bi bi-calendar-event"></i>
|
||||
</div>
|
||||
<div class="cm-info-card-body">
|
||||
<div class="cm-info-card-label">Date</div>
|
||||
<div class="cm-info-card-value">
|
||||
{{ $container->container_date ? $container->container_date->format('d-m-Y') : '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cm-info-card cm-card-excel">
|
||||
<div class="cm-info-card-icon">
|
||||
<i class="bi bi-file-earmark-excel"></i>
|
||||
</div>
|
||||
<div class="cm-info-card-body">
|
||||
<div class="cm-info-card-label">Excel File</div>
|
||||
<div class="cm-info-card-value">
|
||||
@if($container->excel_file)
|
||||
<a href="{{ url($container->excel_file) }}" target="_blank" style="color:#0f172a;text-decoration:none;">
|
||||
Download / View
|
||||
</a>
|
||||
@else
|
||||
<span style="color:#b0b8cc;">Not uploaded</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$totalCtn = 0;
|
||||
$totalQty = 0;
|
||||
$totalCbm = 0.0;
|
||||
$totalKg = 0.0;
|
||||
|
||||
if(!$container->rows->isEmpty()){
|
||||
foreach ($container->rows as $row) {
|
||||
if (!is_array($row->data)) continue;
|
||||
foreach ($row->data as $h => $v) {
|
||||
$norm = strtoupper(str_replace([' ', '/', '-', '.'],'', $h));
|
||||
$val = is_numeric(str_replace([','], '', $v)) ? floatval(str_replace([','], '', $v)) : 0;
|
||||
|
||||
if (str_contains($norm, 'TOTALCTN') || $norm === 'CTN' || str_contains($norm,'TOTALCNTR') || str_contains($norm,'TOTALCARTON')) {
|
||||
$totalCtn += $val;
|
||||
}
|
||||
|
||||
if (
|
||||
str_contains($norm,'TOTALQTY') ||
|
||||
str_contains($norm,'ITLQTY') ||
|
||||
str_contains($norm,'TTLQTY')
|
||||
) {
|
||||
$totalQty += $val;
|
||||
}
|
||||
|
||||
if (
|
||||
str_contains($norm,'TOTALCBM') ||
|
||||
str_contains($norm,'TTLCBM') ||
|
||||
str_contains($norm,'ITLCBM')
|
||||
) {
|
||||
$totalCbm += $val;
|
||||
}
|
||||
|
||||
if (
|
||||
str_contains($norm,'TOTALKG') ||
|
||||
str_contains($norm,'TTKG')
|
||||
) {
|
||||
$totalKg += $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if(!$container->rows->isEmpty())
|
||||
<div class="cm-total-cards-row">
|
||||
<div class="cm-total-card cm-total-card-ctn">
|
||||
<div class="cm-total-icon">
|
||||
<i class="bi bi-box"></i>
|
||||
</div>
|
||||
<div class="cm-total-text">
|
||||
<div class="cm-total-label">Total CTN</div>
|
||||
<div class="cm-total-value">
|
||||
{{ number_format($totalCtn, 0) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cm-total-card cm-total-card-qty">
|
||||
<div class="cm-total-icon">
|
||||
<i class="bi bi-check-circle"></i>
|
||||
</div>
|
||||
<div class="cm-total-text">
|
||||
<div class="cm-total-label">Total QTY</div>
|
||||
<div class="cm-total-value">
|
||||
{{ number_format($totalQty, 0) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cm-total-card cm-total-card-cbm">
|
||||
<div class="cm-total-icon">
|
||||
<i class="bi bi-border-width"></i>
|
||||
</div>
|
||||
<div class="cm-total-text">
|
||||
<div class="cm-total-label">Total CBM</div>
|
||||
<div class="cm-total-value">
|
||||
{{ number_format($totalCbm, 3) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cm-total-card cm-total-card-kg">
|
||||
<div class="cm-total-icon">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
</div>
|
||||
<div class="cm-total-text">
|
||||
<div class="cm-total-label">Total KG</div>
|
||||
<div class="cm-total-value">
|
||||
{{ number_format($totalKg, 2) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@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
|
||||
|
||||
<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>
|
||||
|
||||
<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] ?? '';
|
||||
|
||||
$norm = strtoupper(str_replace([' ', '/', '-', '.'],'', $heading));
|
||||
|
||||
$isCtn = str_contains($norm, 'CTN');
|
||||
|
||||
$isTotalQty = (
|
||||
str_contains($norm, 'TOTALQTY') ||
|
||||
str_contains($norm, 'ITLQTY') ||
|
||||
str_contains($norm, 'TTLQTY')
|
||||
);
|
||||
$isQty = !$isTotalQty && (
|
||||
str_contains($norm, 'QTY') ||
|
||||
str_contains($norm, 'PCS') ||
|
||||
str_contains($norm, 'PIECES')
|
||||
);
|
||||
|
||||
$isTotalCbm = (
|
||||
str_contains($norm, 'TOTALCBM') ||
|
||||
str_contains($norm, 'TTLCBM') ||
|
||||
str_contains($norm, 'ITLCBM')
|
||||
);
|
||||
$isCbm = !$isTotalCbm && str_contains($norm, 'CBM');
|
||||
|
||||
$isTotalKg = (
|
||||
str_contains($norm, 'TOTALKG') ||
|
||||
str_contains($norm, 'TTKG')
|
||||
);
|
||||
$isKg = !$isTotalKg && (str_contains($norm, 'KG') || str_contains($norm, 'WEIGHT'));
|
||||
|
||||
$isPrice = (str_contains($norm, 'PRICE') || str_contains($norm, 'RATE'));
|
||||
|
||||
$isAmount = (
|
||||
str_contains($norm, 'AMOUNT') ||
|
||||
str_contains($norm, 'TTLAMOUNT') ||
|
||||
str_contains($norm, 'TOTALAMOUNT')
|
||||
);
|
||||
|
||||
$isTotalColumn = $isTotalQty || $isTotalCbm || $isTotalKg || $isAmount;
|
||||
$isLockedByInvoice = in_array($row->row_index, $lockedRowIndexes ?? []);
|
||||
$isReadOnly = $isTotalColumn || $container->status !== 'pending' || $isLockedByInvoice;
|
||||
@endphp
|
||||
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="cm-cell-input {{ $isReadOnly ? 'cm-cell-readonly' : '' }}"
|
||||
name="rows[{{ $row->id }}][{{ $heading }}]"
|
||||
value="{{ $value }}"
|
||||
data-row-id="{{ $row->id }}"
|
||||
data-col-key="{{ $heading }}"
|
||||
data-ctn="{{ $isCtn ? '1' : '0' }}"
|
||||
data-qty="{{ $isQty ? '1' : '0' }}"
|
||||
data-ttlqty="{{ $isTotalQty ? '1' : '0' }}"
|
||||
data-cbm="{{ $isCbm ? '1' : '0' }}"
|
||||
data-ttlcbm="{{ $isTotalCbm ? '1' : '0' }}"
|
||||
data-kg="{{ $isKg ? '1' : '0' }}"
|
||||
data-ttlkg="{{ $isTotalKg ? '1' : '0' }}"
|
||||
data-price="{{ $isPrice ? '1' : '0' }}"
|
||||
data-amount="{{ $isAmount ? '1' : '0' }}"
|
||||
{{ $isReadOnly ? 'readonly' : '' }}
|
||||
>
|
||||
</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!$container->rows->isEmpty())
|
||||
<button
|
||||
id="cmSaveBtnFloating"
|
||||
class="cm-save-btn-floating {{ $container->status !== 'pending' ? 'cm-disabled' : '' }}"
|
||||
{{ $container->status !== 'pending' ? 'disabled' : '' }}
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<div id="cmToast" class="cm-toast"></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';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const form = document.getElementById('cm-edit-rows-form');
|
||||
const btn = document.getElementById('cmSaveBtnFloating');
|
||||
const toast = document.getElementById('cmToast');
|
||||
const table = document.getElementById('cmExcelTable');
|
||||
|
||||
function showToast(message, isError = false) {
|
||||
if (!toast) return;
|
||||
toast.textContent = message;
|
||||
toast.style.background = isError ? '#b91c1c' : '#0f172a';
|
||||
toast.classList.add('cm-show');
|
||||
setTimeout(() => toast.classList.remove('cm-show'), 2500);
|
||||
}
|
||||
|
||||
// फक्त number काढण्यासाठी helper, पण cell मध्ये format नाही करणार
|
||||
function parseNumber(str) {
|
||||
if (!str) return 0;
|
||||
const cleaned = String(str).replace(/,/g, '').trim();
|
||||
const val = parseFloat(cleaned);
|
||||
return isNaN(val) ? 0 : val;
|
||||
}
|
||||
|
||||
function recalcRow(row) {
|
||||
const inputs = row.querySelectorAll('.cm-cell-input');
|
||||
|
||||
let ctn = 0, qty = 0, ttlQty = 0;
|
||||
let cbm = 0, ttlCbm = 0;
|
||||
let kg = 0, ttlKg = 0;
|
||||
let price = 0, amount = 0;
|
||||
|
||||
let ctnInput = null,
|
||||
qtyInput = null,
|
||||
ttlQtyInput = null,
|
||||
cbmInput = null,
|
||||
ttlCbmInput = null,
|
||||
kgInput = null,
|
||||
ttlKgInput = null,
|
||||
priceInput = null,
|
||||
amountInput = null;
|
||||
|
||||
inputs.forEach(inp => {
|
||||
const val = parseNumber(inp.value);
|
||||
|
||||
if (inp.dataset.ctn === '1') {
|
||||
ctn = val;
|
||||
ctnInput = inp;
|
||||
} else if (inp.dataset.qty === '1') {
|
||||
qty = val;
|
||||
qtyInput = inp;
|
||||
} else if (inp.dataset.ttlqty === '1') {
|
||||
ttlQty = val;
|
||||
ttlQtyInput = inp;
|
||||
} else if (inp.dataset.cbm === '1') {
|
||||
cbm = val;
|
||||
cbmInput = inp;
|
||||
} else if (inp.dataset.ttlcbm === '1') {
|
||||
ttlCbm = val;
|
||||
ttlCbmInput = inp;
|
||||
} else if (inp.dataset.kg === '1') {
|
||||
kg = val;
|
||||
kgInput = inp;
|
||||
} else if (inp.dataset.ttlkg === '1') {
|
||||
ttlKg = val;
|
||||
ttlKgInput = inp;
|
||||
} else if (inp.dataset.price === '1') {
|
||||
price = val;
|
||||
priceInput = inp;
|
||||
} else if (inp.dataset.amount === '1') {
|
||||
amount = val;
|
||||
amountInput = inp;
|
||||
}
|
||||
});
|
||||
|
||||
// इथे आपण फक्त VALUE बदलतो, कोणतंही toFixed नाही वापरत
|
||||
if (ttlQtyInput && ctnInput && qtyInput) {
|
||||
const newTtlQty = ctn * qty;
|
||||
ttlQtyInput.value = newTtlQty === 0 ? '' : String(newTtlQty);
|
||||
ttlQty = newTtlQty;
|
||||
}
|
||||
|
||||
if (ttlCbmInput && cbmInput && ctnInput) {
|
||||
const newTtlCbm = cbm * ctn;
|
||||
ttlCbmInput.value = newTtlCbm === 0 ? '' : String(newTtlCbm);
|
||||
ttlCbm = newTtlCbm;
|
||||
}
|
||||
|
||||
if (ttlKgInput && kgInput && ctnInput) {
|
||||
const newTtlKg = kg * ctn;
|
||||
ttlKgInput.value = newTtlKg === 0 ? '' : String(newTtlKg);
|
||||
ttlKg = newTtlKg;
|
||||
}
|
||||
|
||||
if (amountInput && priceInput && ttlQtyInput) {
|
||||
const newAmount = price * ttlQty;
|
||||
amountInput.value = newAmount === 0 ? '' : String(newAmount);
|
||||
amount = newAmount;
|
||||
}
|
||||
}
|
||||
|
||||
if (table) {
|
||||
table.addEventListener('input', function (e) {
|
||||
const target = e.target;
|
||||
if (!target.classList.contains('cm-cell-input')) return;
|
||||
|
||||
if (target.classList.contains('cm-cell-readonly') || target.hasAttribute('readonly')) {
|
||||
target.blur();
|
||||
return;
|
||||
}
|
||||
|
||||
const row = target.closest('tr');
|
||||
if (row) {
|
||||
recalcRow(row);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (form && btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
if (btn.classList.contains('cm-disabled') || btn.hasAttribute('disabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
btn.classList.add('cm-disabled');
|
||||
const formData = new FormData(form);
|
||||
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-TOKEN': document.querySelector('input[name="_token"]').value
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(async res => {
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text || 'Failed to save');
|
||||
}
|
||||
return res.json().catch(() => ({}));
|
||||
})
|
||||
.then(() => {
|
||||
showToast('Changes saved successfully.');
|
||||
})
|
||||
.catch(() => {
|
||||
showToast('Error while saving changes.', true);
|
||||
})
|
||||
.finally(() => {
|
||||
btn.classList.remove('cm-disabled');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
overflow-x: hidden; /* Prevent horizontal scroll on body */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
@@ -22,7 +22,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* New Stats Container */
|
||||
.stats-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
@@ -43,9 +42,7 @@
|
||||
min-height: 70px;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.stat-card:hover { transform: translateY(-2px); }
|
||||
|
||||
.stat-card.warning {
|
||||
border-left-color: #f59e0b;
|
||||
@@ -57,16 +54,6 @@
|
||||
background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
|
||||
}
|
||||
|
||||
.stat-card.danger {
|
||||
border-left-color: #ef4444;
|
||||
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
|
||||
}
|
||||
|
||||
.stat-card.info {
|
||||
border-left-color: #3b82f6;
|
||||
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
|
||||
}
|
||||
|
||||
.stat-card.secondary {
|
||||
border-left-color: #8b5cf6;
|
||||
background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);
|
||||
@@ -91,46 +78,19 @@
|
||||
.stat-card.warning .stat-icon {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
.stat-card.warning .stat-icon i {
|
||||
color: #f59e0b;
|
||||
}
|
||||
.stat-card.warning .stat-icon i { color: #f59e0b; }
|
||||
|
||||
.stat-card.success .stat-icon {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
.stat-card.success .stat-icon i {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.stat-card.danger .stat-icon {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.stat-card.danger .stat-icon i {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.stat-card.info .stat-icon {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.stat-card.info .stat-icon i {
|
||||
color: #3b82f6;
|
||||
}
|
||||
.stat-card.success .stat-icon i { color: #10b981; }
|
||||
|
||||
.stat-card.secondary .stat-icon {
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
}
|
||||
.stat-card.secondary .stat-icon i { color: #8b5cf6; }
|
||||
|
||||
.stat-card.secondary .stat-icon i {
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
.stat-content { flex: 1; }
|
||||
|
||||
.stat-value {
|
||||
font-size: 22px;
|
||||
@@ -147,14 +107,13 @@
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Updated Search Container - Wider with icon on left */
|
||||
.search-container {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 10px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
width: 350px; /* Increased width */
|
||||
width: 350px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -210,7 +169,6 @@
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Updated Table Styles - Fixed horizontal scroll */
|
||||
.table-glass {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 10px;
|
||||
@@ -219,7 +177,6 @@
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Single gradient for entire header - Blue to Purple */
|
||||
.table thead {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||
}
|
||||
@@ -232,20 +189,9 @@
|
||||
border: none;
|
||||
font-family: 'Inter', sans-serif;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #667eea 0%);;
|
||||
|
||||
background: linear-gradient(135deg, #667eea 0%);
|
||||
}
|
||||
|
||||
/* Remove individual curved borders */
|
||||
.table-header:first-child {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
.table-header:last-child {
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
/* Apply rounded corners to the entire header container */
|
||||
.table-container thead tr:first-child th:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
@@ -254,7 +200,6 @@
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
/* Updated Table Column Alignment */
|
||||
.table > :not(caption) > * > * {
|
||||
padding: 14px 12px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
@@ -263,44 +208,35 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Center align specific columns */
|
||||
.table > :not(caption) > * > *:nth-child(2), /* Customer ID */
|
||||
.table > :not(caption) > * > *:nth-child(3), /* Orders */
|
||||
.table > :not(caption) > * > *:nth-child(4), /* Total */
|
||||
.table > :not(caption) > * > *:nth-child(5) { /* Create Date */
|
||||
.table > :not(caption) > * > *:nth-child(2),
|
||||
.table > :not(caption) > * > *:nth-child(3),
|
||||
.table > :not(caption) > * > *:nth-child(4),
|
||||
.table > :not(caption) > * > *:nth-child(5) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Customer Info column should remain left-aligned */
|
||||
.table > :not(caption) > * > *:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
.table > :not(caption) > * > *:first-child { text-align: left; }
|
||||
|
||||
/* Status and Actions columns should remain as is */
|
||||
.table > :not(caption) > * > *:nth-child(6), /* Status */
|
||||
.table > :not(caption) > * > *:nth-child(7) { /* Actions */
|
||||
.table > :not(caption) > * > *:nth-child(6),
|
||||
.table > :not(caption) > * > *:nth-child(7),
|
||||
.table > :not(caption) > * > *:nth-child(8),
|
||||
.table > :not(caption) > * > *:nth-child(9) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Updated header alignment to match */
|
||||
|
||||
.table-header:nth-child(2),
|
||||
.table-header:nth-child(3),
|
||||
.table-header:nth-child(4),
|
||||
.table-header:nth-child(5) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Customer Info header stays left */
|
||||
.table-header:first-child {
|
||||
text-align: Center;
|
||||
}
|
||||
|
||||
/* Status and Actions headers stay centered */
|
||||
.table-header:nth-child(5),
|
||||
.table-header:nth-child(6),
|
||||
.table-header:nth-child(7) {
|
||||
.table-header:nth-child(7),
|
||||
.table-header:nth-child(8),
|
||||
.table-header:nth-child(9) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table-header:first-child { text-align: Center; }
|
||||
|
||||
.customer-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -374,7 +310,7 @@
|
||||
|
||||
.customer-info-column {
|
||||
min-width: 220px;
|
||||
max-width: 220px; /* Added max-width to prevent overflow */
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.table tbody tr {
|
||||
@@ -391,31 +327,25 @@
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Remove customer-stats since we're adding columns */
|
||||
|
||||
/* Enhanced table styling - Fixed horizontal scroll */
|
||||
.table-container {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
width: 100%; /* Ensure container takes full width */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Fix table responsiveness */
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Ensure table doesn't exceed container */
|
||||
.table {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 0;
|
||||
table-layout: auto; /* Changed to auto for better column distribution */
|
||||
table-layout: auto;
|
||||
}
|
||||
|
||||
/* Fix for search and filter section */
|
||||
.search-filter-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -439,11 +369,10 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* New columns styling */
|
||||
.orders-column, .total-column, .customer-id-column, .create-date-column {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
min-width: 80px; /* Added minimum widths for consistency */
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.orders-count {
|
||||
@@ -458,7 +387,6 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---------- Pagination Styles ---------- */
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -485,14 +413,10 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
.pagination-img-btn {
|
||||
background: #fff;
|
||||
border: 1px solid #e3eaf6;
|
||||
color: #1a2951;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
@@ -500,20 +424,19 @@
|
||||
justify-content: center;
|
||||
min-width: 40px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
.pagination-img-btn:hover:not(:disabled) {
|
||||
background: #1a2951;
|
||||
color: white;
|
||||
border-color: #1a2951;
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
.pagination-img-btn:disabled {
|
||||
background: #f8fafc;
|
||||
color: #cbd5e0;
|
||||
border-color: #e2e8f0;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.pagination-page-btn {
|
||||
@@ -551,61 +474,11 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination-ellipsis {
|
||||
color: #9ba5bb;
|
||||
font-size: 13px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
/* Image-based pagination buttons */
|
||||
.pagination-img-btn {
|
||||
background: #fff;
|
||||
border: 1px solid #e3eaf6;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 40px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pagination-img-btn:hover:not(:disabled) {
|
||||
background: #1a2951;
|
||||
border-color: #1a2951;
|
||||
}
|
||||
|
||||
.pagination-img-btn:disabled {
|
||||
background: #f8fafc;
|
||||
border-color: #e2e8f0;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.pagination-img-btn img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
filter: brightness(0) saturate(100%) invert(26%) sepia(89%) saturate(748%) hue-rotate(201deg) brightness(93%) contrast(89%);
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.pagination-img-btn:hover:not(:disabled) img {
|
||||
filter: brightness(0) saturate(100%) invert(100%) sepia(100%) saturate(0%) hue-rotate(288deg) brightness(106%) contrast(101%);
|
||||
}
|
||||
|
||||
.pagination-img-btn:disabled img {
|
||||
filter: brightness(0) saturate(100%) invert(84%) sepia(8%) saturate(165%) hue-rotate(179deg) brightness(89%) contrast(86%);
|
||||
}
|
||||
|
||||
/* Mobile responsive fixes */
|
||||
@media (max-width: 1200px) {
|
||||
.table > :not(caption) > * > * {
|
||||
padding: 12px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.customer-info-column {
|
||||
min-width: 180px;
|
||||
max-width: 180px;
|
||||
@@ -613,65 +486,42 @@
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.search-container {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.stats-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.search-container { width: 280px; }
|
||||
.stats-container { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.stats-container { grid-template-columns: repeat(2, 1fr); }
|
||||
.search-filter-container {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-section { justify-content: center; }
|
||||
.search-container { width: 100%; }
|
||||
.filter-section {
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pagination-controls { justify-content: center; }
|
||||
.table > :not(caption) > * > * {
|
||||
padding: 10px 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.customer-info-column {
|
||||
min-width: 150px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.customer-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
@@ -680,19 +530,12 @@
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.stats-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stats-container { grid-template-columns: 1fr; }
|
||||
.table-responsive { font-size: 12px; }
|
||||
.customer-info-column {
|
||||
min-width: 120px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.premium-badge,
|
||||
.regular-badge,
|
||||
.status-badge {
|
||||
@@ -703,14 +546,11 @@
|
||||
</style>
|
||||
|
||||
<div class="container-fluid">
|
||||
<!-- Header - Removed gradient -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 style="color: #2c3e50; font-weight: 700; font-family: 'Inter', sans-serif;">Customer List</h4>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards with NEW DESIGN -->
|
||||
<div class="stats-container">
|
||||
<!-- Total Customers -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<i class="bi bi-people-fill"></i>
|
||||
@@ -721,7 +561,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New This Month -->
|
||||
<div class="stat-card warning">
|
||||
<div class="stat-icon">
|
||||
<i class="bi bi-person-plus"></i>
|
||||
@@ -739,7 +578,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Customers -->
|
||||
<div class="stat-card success">
|
||||
<div class="stat-icon">
|
||||
<i class="bi bi-activity"></i>
|
||||
@@ -755,7 +593,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Premium Customers -->
|
||||
<div class="stat-card secondary">
|
||||
<div class="stat-icon">
|
||||
<i class="bi bi-award-fill"></i>
|
||||
@@ -772,10 +609,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter Section -->
|
||||
<div class="glass-card p-3 mb-3">
|
||||
<div class="search-filter-container">
|
||||
<!-- Search Section - Wider with icon on left -->
|
||||
<div class="search-section">
|
||||
<form method="GET" action="{{ route('admin.customers.index') }}" class="d-flex align-items-center">
|
||||
<div class="search-container">
|
||||
@@ -792,7 +627,6 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Filter Section -->
|
||||
<div class="filter-section">
|
||||
<a href="{{ route('admin.customers.index', ['status'=>'active', 'search'=>$search ?? '']) }}"
|
||||
class="filter-btn {{ ($status ?? '') == 'active' ? 'active' : '' }}">
|
||||
@@ -818,7 +652,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customer List Table -->
|
||||
<div class="table-container">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
@@ -828,8 +661,9 @@
|
||||
<th class="table-header">Customer ID</th>
|
||||
<th class="table-header">Orders</th>
|
||||
<th class="table-header">Order Total</th>
|
||||
<th class="table-header">Total Payable</th> {{-- NEW --}}
|
||||
<th class="table-header">Remaining</th> {{-- NEW --}}
|
||||
<th class="table-header">GST Amount</th>
|
||||
<th class="table-header">Total Paid</th>
|
||||
<th class="table-header">Remaining</th>
|
||||
<th class="table-header">Create Date</th>
|
||||
<th class="table-header">Status</th>
|
||||
<th class="table-header" width="100">Actions</th>
|
||||
@@ -839,113 +673,119 @@
|
||||
<tbody id="customersTableBody">
|
||||
@forelse($customers as $c)
|
||||
@php
|
||||
// Invoice total (with GST)
|
||||
$totalPayable = $c->invoices->sum('final_amount_with_gst');
|
||||
// 1) Orders = total invoice count
|
||||
$ordersCount = $c->invoices->count();
|
||||
|
||||
// Total paid via installments
|
||||
$totalPaid = $c->invoices
|
||||
->flatMap(fn($inv) => $inv->installments)
|
||||
->sum('amount');
|
||||
// 2) Order Total = सर्व invoices च्या charge groups चा base total (without GST)
|
||||
$orderTotal = $c->invoices->sum(function($invoice) {
|
||||
return $invoice->chargeGroups->sum('total_charge');
|
||||
});
|
||||
|
||||
// Remaining amount
|
||||
$remainingAmount = max($totalPayable - $totalPaid, 0);
|
||||
// 3) GST Amount = सर्व invoices च्या gst_amount चा sum
|
||||
$gstTotal = $c->invoices->sum('gst_amount');
|
||||
|
||||
// 3) Total Payable = customer ने किती paid केले (installments sum)
|
||||
$totalPaid = $c->invoices->flatMap->installments->sum('amount');
|
||||
|
||||
// 4) Remaining = grand_total_with_charges - paid
|
||||
$grandTotal = $c->invoices->sum(function($invoice) {
|
||||
$base = $invoice->chargeGroups->sum('total_charge');
|
||||
$gst = (float)($invoice->gst_amount ?? 0);
|
||||
return $base + $gst;
|
||||
});
|
||||
$remainingAmount = max($grandTotal - $totalPaid, 0);
|
||||
@endphp
|
||||
<tr>
|
||||
<!-- Customer Info Column -->
|
||||
<td class="customer-info-column">
|
||||
<div class="d-flex align-items-start">
|
||||
<div class="customer-avatar me-3">
|
||||
{{ strtoupper(substr($c->customer_name,0,1)) }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold">{{ $c->customer_name }}</div>
|
||||
@if($c->customer_type == 'premium')
|
||||
<span class="premium-badge">Premium Customer</span>
|
||||
@else
|
||||
<span class="regular-badge">Regular Customer</span>
|
||||
@endif
|
||||
<div class="customer-details mt-1">
|
||||
{{ $c->email }}<br>
|
||||
{{ $c->mobile_no }}
|
||||
<tr>
|
||||
<td class="customer-info-column">
|
||||
<div class="d-flex align-items-start">
|
||||
<div class="customer-avatar me-3">
|
||||
{{ strtoupper(substr($c->customer_name,0,1)) }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold">{{ $c->customer_name }}</div>
|
||||
@if($c->customer_type == 'premium')
|
||||
<span class="premium-badge">Premium Customer</span>
|
||||
@else
|
||||
<span class="regular-badge">Regular Customer</span>
|
||||
@endif
|
||||
<div class="customer-details mt-1">
|
||||
{{ $c->email }}<br>
|
||||
{{ $c->mobile_no }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</td>
|
||||
|
||||
<!-- Customer ID -->
|
||||
<td class="customer-id-column">
|
||||
<span class="fw-bold text-primary">{{ $c->customer_id }}</span>
|
||||
</td>
|
||||
<td class="customer-id-column">
|
||||
<span class="fw-bold text-primary">{{ $c->customer_id }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Orders Column -->
|
||||
<td class="orders-column">
|
||||
<span class="orders-count">{{ $c->orders->count() }}</span>
|
||||
</td>
|
||||
<td class="orders-column">
|
||||
<span class="orders-count">{{ $ordersCount }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Total Column -->
|
||||
<td class="total-column">
|
||||
<span class="total-amount">
|
||||
₹{{ number_format($c->orders->sum('ttl_amount'), 2) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="total-column">
|
||||
<span class="total-amount">
|
||||
₹{{ number_format($totalPayable, 2) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="total-column">
|
||||
@if($remainingAmount > 0)
|
||||
<span class="text-danger fw-bold">
|
||||
₹{{ number_format($remainingAmount, 2) }}
|
||||
<td class="total-column">
|
||||
<span class="total-amount">
|
||||
₹{{ number_format($orderTotal, 2) }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-success fw-bold">
|
||||
₹0.00
|
||||
</td>
|
||||
|
||||
<td class="total-column">
|
||||
<span class="total-amount">
|
||||
₹{{ number_format($gstTotal, 2) }}
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
</td>
|
||||
|
||||
<td class="total-column">
|
||||
<span class="total-amount">
|
||||
₹{{ number_format($totalPaid, 2) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
|
||||
<td class="total-column">
|
||||
@if($remainingAmount > 0)
|
||||
<span class="text-danger fw-bold">
|
||||
₹{{ number_format($remainingAmount, 2) }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-success fw-bold">
|
||||
₹0.00
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<td class="create-date-column">
|
||||
<span class="text-muted">{{ $c->created_at ? $c->created_at->format('d-m-Y') : '-' }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Create Date -->
|
||||
<td class="create-date-column">
|
||||
<span class="text-muted">{{ $c->created_at ? $c->created_at->format('d-m-Y') : '-' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
@if($c->status === 'active')
|
||||
<span class="status-badge active-status">Active</span>
|
||||
@else
|
||||
<span class="status-badge inactive-status">Inactive</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<!-- Status -->
|
||||
<td>
|
||||
@if($c->status === 'active')
|
||||
<span class="status-badge active-status">Active</span>
|
||||
@else
|
||||
<span class="status-badge inactive-status">Inactive</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex justify-content-center">
|
||||
<a href="{{ route('admin.customers.view', $c->id) }}"
|
||||
class="action-btn" title="View">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
|
||||
<!-- Actions -->
|
||||
<td>
|
||||
<div class="d-flex justify-content-center">
|
||||
<a href="{{ route('admin.customers.view', $c->id) }}"
|
||||
class="action-btn" title="View">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
|
||||
<form action="{{ route('admin.customers.status', $c->id) }}"
|
||||
method="POST" style="display:inline-block;">
|
||||
@csrf
|
||||
<button class="action-btn" title="Toggle Status" type="submit">
|
||||
<i class="bi bi-power"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<form action="{{ route('admin.customers.status', $c->id) }}"
|
||||
method="POST" style="display:inline-block;">
|
||||
@csrf
|
||||
<button class="action-btn" title="Toggle Status" type="submit">
|
||||
<i class="bi bi-power"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4">
|
||||
<td colspan="10" class="text-center py-4">
|
||||
<i class="bi bi-people display-4 text-muted d-block mb-2"></i>
|
||||
<span class="text-muted">No customers found.</span>
|
||||
</td>
|
||||
@@ -956,14 +796,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
<div class="pagination-container">
|
||||
<div class="pagination-info" id="pageInfo">
|
||||
Showing {{ $customers->firstItem() ?? 0 }} to {{ $customers->lastItem() ?? 0 }} of {{ $customers->total() }} entries
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<button class="pagination-img-btn" id="prevPageBtn" title="Previous page" {{ $customers->onFirstPage() ? 'disabled' : '' }}>
|
||||
<!-- Left arrow 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"/>
|
||||
</svg>
|
||||
@@ -977,7 +815,6 @@
|
||||
@endfor
|
||||
</div>
|
||||
<button class="pagination-img-btn" id="nextPageBtn" title="Next page" {{ $customers->hasMorePages() ? '' : 'disabled' }}>
|
||||
<!-- Right arrow 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"/>
|
||||
</svg>
|
||||
@@ -988,7 +825,6 @@
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add hover effects to table rows
|
||||
const tableRows = document.querySelectorAll('.table tbody tr');
|
||||
tableRows.forEach(row => {
|
||||
row.addEventListener('mouseenter', function() {
|
||||
@@ -1000,7 +836,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Pagination button handlers
|
||||
document.getElementById('prevPageBtn').addEventListener('click', function() {
|
||||
@if(!$customers->onFirstPage())
|
||||
window.location.href = '{{ $customers->previousPageUrl() }}';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@
|
||||
@section('page-title', 'Customer Details')
|
||||
|
||||
@section('content')
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
@@ -33,10 +32,12 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(45deg,
|
||||
rgba(102, 126, 234, 0.03) 0%,
|
||||
rgba(118, 75, 162, 0.03) 50%,
|
||||
rgba(16, 185, 129, 0.03) 100%);
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
rgba(102, 126, 234, 0.03) 0%,
|
||||
rgba(118, 75, 162, 0.03) 50%,
|
||||
rgba(16, 185, 129, 0.03) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -81,10 +82,12 @@
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: linear-gradient(45deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.1) 50%,
|
||||
transparent 100%);
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.1) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
animation: headerShimmer 8s infinite linear;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
@@ -333,7 +336,7 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Buttons - FIXED POSITIONING */
|
||||
/* Buttons */
|
||||
.btn-back {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
@@ -358,10 +361,12 @@
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.2),
|
||||
transparent);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.2),
|
||||
transparent
|
||||
);
|
||||
transition: left 0.6s ease;
|
||||
}
|
||||
|
||||
@@ -397,10 +402,12 @@
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.2),
|
||||
transparent);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.2),
|
||||
transparent
|
||||
);
|
||||
transition: left 0.6s ease;
|
||||
}
|
||||
|
||||
@@ -497,7 +504,7 @@
|
||||
.animation-delay-3 { animation-delay: 0.3s; }
|
||||
.animation-delay-4 { animation-delay: 0.4s; }
|
||||
|
||||
/* Header Button Container - FIXED */
|
||||
/* Header Button Container */
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -511,47 +518,48 @@
|
||||
padding: 25px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.customer-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
|
||||
.page-header {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
.stats-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
.stats-value {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
|
||||
.header-actions {
|
||||
justify-content: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
|
||||
.page-header .row {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.btn-back, .btn-outline-secondary {
|
||||
.btn-back,
|
||||
.btn-outline-secondary {
|
||||
padding: 10px 20px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
|
||||
.header-actions {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.header-actions a {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
@@ -560,8 +568,7 @@
|
||||
</style>
|
||||
|
||||
<div class="container py-4">
|
||||
|
||||
{{-- HEADER - FIXED BUTTON POSITION --}}
|
||||
{{-- HEADER --}}
|
||||
<div class="page-header animate-fade-in">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
@@ -586,7 +593,7 @@
|
||||
<div class="col-auto">
|
||||
<div class="avatar-container">
|
||||
<div class="customer-avatar">
|
||||
{{ strtoupper(substr($customer->customer_name,0,1)) }}
|
||||
{{ strtoupper(substr($customer->customer_name, 0, 1)) }}
|
||||
</div>
|
||||
<div class="avatar-status {{ $customer->status == 'active' ? 'status-active' : 'status-inactive' }}"></div>
|
||||
</div>
|
||||
@@ -703,43 +710,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- {{-- Total Payable --}}
|
||||
<div class="stats-card amount">
|
||||
<div class="stats-icon">
|
||||
<i class="bi bi-wallet2"></i>
|
||||
</div>
|
||||
<div class="stats-value">₹{{ number_format($totalPayable, 2) }}</div>
|
||||
<div class="stats-label">Total Payable</div>
|
||||
</div>
|
||||
|
||||
{{-- Total Remaining --}}
|
||||
<div class="stats-card marks">
|
||||
<div class="stats-icon">
|
||||
<i class="bi bi-exclamation-circle"></i>
|
||||
</div>
|
||||
<div class="stats-value">₹{{ number_format($totalRemaining, 2) }}</div>
|
||||
<div class="stats-label">Remaining Amount</div>
|
||||
</div> -->
|
||||
{{-- Total Payable --}}
|
||||
<div class="col-md-4 animate-fade-in animation-delay-3">
|
||||
<div class="stats-card marks">
|
||||
<div class="stats-icon">
|
||||
<i class="bi bi-hash"></i>
|
||||
<i class="bi bi-wallet2"></i>
|
||||
</div>
|
||||
<div class="stats-value">₹{{ number_format($totalPayable, 2) }}</div>
|
||||
<div class="stats-label">Total Payable</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Remaining Amount --}}
|
||||
<div class="col-md-4 animate-fade-in animation-delay-3">
|
||||
<div class="stats-card marks">
|
||||
<div class="stats-icon">
|
||||
<i class="bi bi-hash"></i>
|
||||
<i class="bi bi-exclamation-circle"></i>
|
||||
</div>
|
||||
<div class="stats-value">₹{{ number_format($totalRemaining, 2) }}</div>
|
||||
<div class="stats-label">Remaining Amount</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{{-- Mark Count --}}
|
||||
<div class="col-md-4 animate-fade-in animation-delay-3">
|
||||
<div class="stats-card marks">
|
||||
@@ -761,7 +753,7 @@
|
||||
<span class="badge bg-primary ms-2">{{ $customer->marks->count() }}</span>
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="section-body">
|
||||
@if($customer->marks->count() == 0)
|
||||
<div class="text-center py-5">
|
||||
@@ -783,20 +775,23 @@
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Add hover effects to interactive elements
|
||||
const interactiveElements = document.querySelectorAll('.info-card, .stats-card, .mark-item');
|
||||
|
||||
|
||||
interactiveElements.forEach(element => {
|
||||
element.addEventListener('mouseenter', function() {
|
||||
this.style.transform = this.classList.contains('mark-item') ? 'translateX(5px)' : 'translateY(-5px)';
|
||||
element.addEventListener('mouseenter', function () {
|
||||
if (this.classList.contains('mark-item')) {
|
||||
this.style.transform = 'translateX(5px)';
|
||||
} else {
|
||||
this.style.transform = 'translateY(-5px)';
|
||||
}
|
||||
});
|
||||
|
||||
element.addEventListener('mouseleave', function() {
|
||||
|
||||
element.addEventListener('mouseleave', function () {
|
||||
this.style.transform = 'translateY(0)';
|
||||
});
|
||||
});
|
||||
@@ -806,7 +801,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
statsValues.forEach(value => {
|
||||
const originalText = value.textContent;
|
||||
value.textContent = '0';
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
value.textContent = originalText;
|
||||
value.style.transform = 'scale(1.1)';
|
||||
@@ -817,5 +812,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
@endsection
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,10 +8,10 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
/*Remove horizontal scroll bar*/
|
||||
html, body {
|
||||
/* html, body {
|
||||
overflow-x: hidden !important;
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
}
|
||||
} */
|
||||
|
||||
/* Invoice Management Styles */
|
||||
.invoice-management-box {
|
||||
@@ -27,7 +27,7 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 17px 17px 0 0;
|
||||
background: #fceeb8ff;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 54px;
|
||||
padding: 15px 26px 10px 22px;
|
||||
border-bottom: 1.4px solid #e8e2cf;
|
||||
@@ -37,7 +37,7 @@
|
||||
.invoice-management-title {
|
||||
font-size: 1.32rem;
|
||||
font-weight: 800;
|
||||
color: #2451af;
|
||||
color: #ffffffff;
|
||||
letter-spacing: .08em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -50,43 +50,50 @@
|
||||
color: #336ad3;
|
||||
}
|
||||
|
||||
/* Tools Row Styling */
|
||||
/* ===== UPDATED FILTER BAR WITH DATE RANGE ===== */
|
||||
.invoice-tools-row {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #edf2f7 100%);
|
||||
padding: 25px 30px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
box-shadow: 0 10px 25px -5px rgba(99, 102, 241, 0.3);
|
||||
position: relative;
|
||||
border-radius: 17px 17px 0 0;
|
||||
top:5px;
|
||||
}
|
||||
|
||||
.filter-bar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Search Box Styling */
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 10px 18px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.08);
|
||||
border: 1.5px solid #e2e8f0;
|
||||
min-width: 380px;
|
||||
flex: 1;
|
||||
max-width: 500px;
|
||||
border-radius: 50px;
|
||||
padding: 8px 20px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.08), inset 0 1px 2px rgba(255,255,255,0.5);
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
flex: 2;
|
||||
min-width: 250px;
|
||||
transition: all 0.3s ease;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.search-box:focus-within {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 4px 20px rgba(59, 130, 246, 0.15);
|
||||
box-shadow: 0 8px 20px rgba(79, 70, 229, 0.25);
|
||||
transform: translateY(-2px);
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.search-box i {
|
||||
color: #64748b;
|
||||
color: #667eea;
|
||||
margin-right: 12px;
|
||||
font-size: 1.2rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
@@ -95,93 +102,196 @@
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
color: #334155;
|
||||
color: #1f2937;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.search-box input::placeholder {
|
||||
color: #94a3b8;
|
||||
color: #9ca3af;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* .search-btn {
|
||||
background: linear-gradient(90deg, #226ad6, #46b4fd 123%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 5px 10px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: 3px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
white-space: nowrap;
|
||||
} */
|
||||
|
||||
.search-btn:hover {
|
||||
background: linear-gradient(90deg, #3264f8, #3acfff 140%);
|
||||
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
/* Filter Selects Styling */
|
||||
.filter-select-wrapper {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
width: 100%;
|
||||
background: white;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
padding: 10px 15px;
|
||||
border: 1px solid rgba(255,255,255,0.5);
|
||||
border-radius: 50px;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
color: #1f2937;
|
||||
outline: none;
|
||||
min-width: 160px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.05);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 400;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23667eea' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 15px center;
|
||||
background-size: 16px;
|
||||
}
|
||||
|
||||
.filter-select:hover {
|
||||
background-color: white;
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.filter-select:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
border-color: white;
|
||||
box-shadow: 0 0 0 3px rgba(255,255,255,0.2);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.create-invoice-btn {
|
||||
background: linear-gradient(90deg, #226ad6, #46b4fd 123%);
|
||||
padding: 12px 28px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
box-shadow: 0 2px 13px #dde7fa42;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
/* ===== NEW: Status dropdown option colors ===== */
|
||||
.filter-select option[value="paid"] {
|
||||
background-color: #d1fae5;
|
||||
color: #065f46;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-select option[value="pending"] {
|
||||
background-color: #fef3c7;
|
||||
color: #d97706;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-select option[value="overdue"] {
|
||||
background-color: #e9d5ff;
|
||||
color: #6b21a8;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-select option[value="paying"] {
|
||||
background-color: #e0e7ff;
|
||||
color: #4338ca;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
.filter-select option[value="all"] {
|
||||
background-color: white;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* For Firefox compatibility */
|
||||
.filter-select option {
|
||||
padding: 8px;
|
||||
}
|
||||
/* ===== END status dropdown option colors ===== */
|
||||
|
||||
/* Date Range Styling */
|
||||
.date-range-wrapper {
|
||||
flex: 1.5;
|
||||
min-width: 280px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
/* background: rgba(255, 255, 255, 0.2); */
|
||||
padding: 5px 12px;
|
||||
border-radius: 50px;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.date-input-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 50px;
|
||||
padding: 5px 12px;
|
||||
flex: 1;
|
||||
border: 1px solid rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
.date-input-container i {
|
||||
color: #667eea;
|
||||
margin-right: 6px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
color: #1f2937;
|
||||
font-family: 'Inter', sans-serif;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.create-invoice-btn:hover {
|
||||
background: linear-gradient(90deg, #3264f8, #3acfff 140%);
|
||||
box-shadow: 0 4px 25px #5ab8f880;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
transform: translateY(-2px);
|
||||
.date-input::placeholder {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1100px) {
|
||||
.filter-bar-container {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.date-range-wrapper {
|
||||
min-width: 260px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.invoice-tools-row {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.filter-bar-container {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.filter-select-wrapper {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.date-range-wrapper {
|
||||
min-width: 100%;
|
||||
flex-direction: column;
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
padding: 0;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-input-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
/* ===== END UPDATED FILTER BAR ===== */
|
||||
|
||||
.invoice-management-main {
|
||||
background: #fff;
|
||||
border-radius: 0 0 17px 17px;
|
||||
@@ -223,7 +333,7 @@
|
||||
|
||||
/* Center all table content */
|
||||
.table thead tr {
|
||||
background: #feebbe !important;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.table thead th:first-child {
|
||||
@@ -237,7 +347,7 @@
|
||||
background: transparent !important;
|
||||
border: none;
|
||||
font-weight: 700;
|
||||
color: #343535;
|
||||
color: #ffffffff;
|
||||
letter-spacing: 0.02em;
|
||||
font-size: 14px;
|
||||
padding: 20px 15px;
|
||||
@@ -258,25 +368,33 @@
|
||||
|
||||
/* Soft blue background for ALL table rows */
|
||||
.table-striped tbody tr {
|
||||
background: #f0f8ff !important;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
background: #f0f8ff !important;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
|
||||
.table-striped tbody tr td {
|
||||
padding: 6px 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.table-striped tbody tr:hover {
|
||||
background: #e6f3ff !important;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
||||
transform: translateY(-0.5px);
|
||||
}
|
||||
|
||||
.table-striped tbody tr:hover {
|
||||
background: #e6f3ff !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Remove striped pattern - all rows same soft blue */
|
||||
.table-striped tbody tr:nth-of-type(odd),
|
||||
.table-striped tbody tr:nth-of-type(even) {
|
||||
background: #f0f8ff !important;
|
||||
}
|
||||
|
||||
/* Center all table cells with proper spacing */
|
||||
|
||||
.table td {
|
||||
padding: 18px 15px;
|
||||
border: none;
|
||||
@@ -291,7 +409,7 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* First and last cell rounded corners */
|
||||
|
||||
.table td:first-child {
|
||||
padding-left: 30px;
|
||||
font-weight: 600;
|
||||
@@ -423,6 +541,10 @@
|
||||
background: url('/images/status-bg-overdue.png') !important;
|
||||
}
|
||||
|
||||
.badge-paying {
|
||||
background: url('/images/status-bg-paying.png') !important;
|
||||
}
|
||||
|
||||
/* Fallback colors if images don't load - ALL WITH SAME SIZE */
|
||||
.badge.badge-paid {
|
||||
background: linear-gradient(135deg, #d1fae5, #a7f3d0) !important;
|
||||
@@ -443,6 +565,13 @@
|
||||
border-color: #8b5cf6 !important;
|
||||
}
|
||||
|
||||
.badge.badge-paying {
|
||||
background: linear-gradient(135deg, #e0e7ff, #c7d2fe) !important;
|
||||
color: #4338ca !important;
|
||||
border-color: #6366f1 !important;
|
||||
}
|
||||
|
||||
|
||||
/* Entry Button - Centered */
|
||||
.btn-entry {
|
||||
background: linear-gradient(135deg, #3492f8 55%, #1256cc 110%);
|
||||
@@ -561,37 +690,6 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Date Range Picker Styles */
|
||||
.date-range-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
background: white;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
outline: none;
|
||||
min-width: 140px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.04);
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.date-input:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Stats Summary - Centered */
|
||||
.stats-summary {
|
||||
display: grid;
|
||||
@@ -942,17 +1040,6 @@
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1200px) {
|
||||
.invoice-tools-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
max-width: 100%;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
@@ -970,10 +1057,6 @@
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.invoice-tools-row {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.table th, .table td {
|
||||
font-size: 13px;
|
||||
padding: 15px 10px;
|
||||
@@ -995,21 +1078,6 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
min-width: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.create-invoice-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.invoice-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
@@ -1025,15 +1093,6 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.date-range-container {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stats-summary {
|
||||
grid-template-columns: 1fr;
|
||||
margin: 20px;
|
||||
@@ -1071,11 +1130,6 @@
|
||||
min-width: auto;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
padding: 8px 15px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1088,39 +1142,58 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- TOOLS ROW - Search, Filter, Create Button -->
|
||||
<!-- UPDATED FILTER BAR WITH DATE RANGE -->
|
||||
<div class="invoice-tools-row">
|
||||
<!-- Search Box with Button -->
|
||||
<div style="display: flex; align-items: center; flex: 1; max-width: 500px; min-width: 380px;">
|
||||
<div class="search-box">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text" id="invoiceSearch" placeholder="Search by invoice number, customer name...">
|
||||
<form method="GET" action="{{ route('admin.invoices.index') }}" style="width: 100%;">
|
||||
<div class="filter-bar-container">
|
||||
<!-- Search Box -->
|
||||
<div class="search-box">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text"
|
||||
id="searchInput"
|
||||
name="search"
|
||||
value="{{ request('search') }}"
|
||||
placeholder="Search Invoices...">
|
||||
</div>
|
||||
|
||||
<!-- Status Filter -->
|
||||
<div class="filter-select-wrapper">
|
||||
<select class="filter-select" id="statusFilter" name="status">
|
||||
<option value="all" {{ request('status')=='all' ? 'selected' : '' }}>All Status</option>
|
||||
<option value="paid" {{ request('status')=='paid' ? 'selected' : '' }}>Paid</option>
|
||||
<option value="pending" {{ request('status')=='pending' ? 'selected' : '' }}>Pending</option>
|
||||
<option value="overdue" {{ request('status')=='overdue' ? 'selected' : '' }}>Overdue</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter (FROM - TO) with dd-mm-yyyy format -->
|
||||
<div class="date-range-wrapper">
|
||||
<div class="date-input-container">
|
||||
<i class="bi bi-calendar3"></i>
|
||||
<input type="text"
|
||||
class="date-input"
|
||||
id="startDate"
|
||||
name="start_date"
|
||||
value="{{ request('start_date') ? date('d-m-Y', strtotime(request('start_date'))) : '' }}"
|
||||
placeholder="dd-mm-yyyy"
|
||||
onfocus="(this.type='date')"
|
||||
onblur="if(!this.value) this.type='text'">
|
||||
</div>
|
||||
<span class="date-separator">to</span>
|
||||
<div class="date-input-container">
|
||||
<i class="bi bi-calendar3"></i>
|
||||
<input type="text"
|
||||
class="date-input"
|
||||
id="endDate"
|
||||
name="end_date"
|
||||
value="{{ request('end_date') ? date('d-m-Y', strtotime(request('end_date'))) : '' }}"
|
||||
placeholder="dd-mm-yyyy"
|
||||
onfocus="(this.type='date')"
|
||||
onblur="if(!this.value) this.type='text'">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filter Group -->
|
||||
<div class="filter-group">
|
||||
<select class="filter-select" id="statusFilter">
|
||||
<option value="">All Status</option>
|
||||
<option value="paid">Paid</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="overdue">Overdue</option>
|
||||
</select>
|
||||
|
||||
<!-- Date Range Picker -->
|
||||
<div class="date-range-container">
|
||||
<input type="date" class="date-input" id="startDate">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date" class="date-input" id="endDate">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Invoice Button -->
|
||||
<!-- <a href="{{ route('admin.invoices.create') }}" class="create-invoice-btn">
|
||||
<i class="bi bi-plus-circle"></i> Create Invoice
|
||||
</a> -->
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="invoice-management-main no-extra-space">
|
||||
@@ -1156,77 +1229,110 @@
|
||||
<div class="card-body p-0">
|
||||
<div class="table-container">
|
||||
<table class="table table-striped align-middle" id="invoicesTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="column-header">#</th>
|
||||
<th class="column-header">Invoice Number</th>
|
||||
<th class="column-header">Customer</th>
|
||||
<th class="column-header">Final Amount</th>
|
||||
<th class="column-header">GST %</th>
|
||||
<th class="column-header">Total w/GST</th>
|
||||
<th class="column-header">Status</th>
|
||||
<th class="column-header">Invoice Date</th>
|
||||
<th class="column-header">Due Date</th>
|
||||
<th class="column-header">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="column-header">#</th>
|
||||
<th class="column-header">Invoice Number</th>
|
||||
<th class="column-header">Customer</th>
|
||||
<th class="column-header">Container</th> {{-- NEW --}}
|
||||
<th class="column-header">Final Amount</th>
|
||||
<th class="column-header">GST Amount</th>
|
||||
<th class="column-header">Total w/GST</th>
|
||||
<th class="column-header">Status</th>
|
||||
<th class="column-header">Invoice Date</th>
|
||||
<th class="column-header">Due Date</th>
|
||||
<th class="column-header">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody id="invoicesTableBody">
|
||||
@php
|
||||
$totalInvoices = $invoices->count();
|
||||
$sortedInvoices = $invoices->sortByDesc('created_at'); // Latest first
|
||||
@endphp
|
||||
|
||||
@forelse($sortedInvoices as $i => $invoice)
|
||||
<tr>
|
||||
<td>{{ $totalInvoices - $i }}</td>
|
||||
<tbody id="invoicesTableBody">
|
||||
@php
|
||||
$totalInvoices = $invoices->count();
|
||||
$sortedInvoices = $invoices->sortByDesc('created_at'); // Latest first
|
||||
@endphp
|
||||
|
||||
@forelse($sortedInvoices as $i => $invoice)
|
||||
<tr>
|
||||
<td>{{ $totalInvoices - $i }}</td>
|
||||
|
||||
<td>
|
||||
<div class="invoice-number-cell">
|
||||
<div class="invoice-icon invoice-icon-{{ (($totalInvoices - $i) % 8) + 1 }}">
|
||||
<i class="bi bi-file-earmark-text"></i>
|
||||
</div>
|
||||
<a href="#" class="invoice-number-link open-invoice-popup" data-id="{{ $invoice->id }}">
|
||||
{{ $invoice->invoice_number }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="invoice-number-cell">
|
||||
<div class="invoice-icon invoice-icon-{{ (($totalInvoices - $i) % 8) + 1 }}">
|
||||
<i class="bi bi-file-earmark-text"></i>
|
||||
</div>
|
||||
<a href="#" class="invoice-number-link open-invoice-popup" data-id="{{ $invoice->id }}">
|
||||
{{ $invoice->invoice_number }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="customer-cell">{{ $invoice->customer_name }}</td>
|
||||
<td class="customer-cell">
|
||||
{{ $invoice->customer_name }}
|
||||
</td>
|
||||
|
||||
<td class="amount-cell">₹{{ number_format($invoice->final_amount, 2) }}</td>
|
||||
<td class="gst-cell">{{ $invoice->gst_percent }}%</td>
|
||||
<td class="amount-cell">₹{{ number_format($invoice->final_amount_with_gst, 2) }}</td>
|
||||
{{-- NEW: Container column --}}
|
||||
<td class="customer-cell">
|
||||
@if($invoice->container)
|
||||
{{ $invoice->container->container_number }}
|
||||
{{-- जर फक्त ID हवी असेल तर:
|
||||
#{{ $invoice->container->id }}
|
||||
--}}
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span class="badge badge-{{ $invoice->status }}">
|
||||
@if($invoice->status == 'paid')
|
||||
<i class="bi bi-check-circle-fill status-icon"></i>
|
||||
@elseif($invoice->status == 'pending')
|
||||
<i class="bi bi-clock-fill status-icon"></i>
|
||||
@elseif($invoice->status == 'overdue')
|
||||
<i class="bi bi-exclamation-triangle-fill status-icon"></i>
|
||||
@endif
|
||||
{{ ucfirst($invoice->status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="amount-cell">
|
||||
₹{{ number_format($invoice->final_amount, 2) }}
|
||||
</td>
|
||||
|
||||
<td class="date-cell">{{ $invoice->invoice_date }}</td>
|
||||
<td class="date-cell">{{ $invoice->due_date }}</td>
|
||||
<td class="amount-cell">
|
||||
₹{{ number_format($invoice->gst_amount, 2) }}
|
||||
</td>
|
||||
|
||||
<td class="amount-cell">
|
||||
₹{{ number_format($invoice->final_amount_with_gst, 2) }}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span class="badge badge-{{ $invoice->status }}">
|
||||
@if($invoice->status == 'paid')
|
||||
<i class="bi bi-check-circle-fill status-icon"></i>
|
||||
@elseif($invoice->status == 'pending')
|
||||
<i class="bi bi-clock-fill status-icon"></i>
|
||||
@elseif($invoice->status == 'paying')
|
||||
<i class="bi bi-arrow-repeat status-icon"></i> {{-- processing icon --}}
|
||||
@elseif($invoice->status == 'overdue')
|
||||
<i class="bi bi-exclamation-triangle-fill status-icon"></i>
|
||||
@endif
|
||||
{{ ucfirst($invoice->status) }}
|
||||
</span>
|
||||
|
||||
</td>
|
||||
|
||||
<td class="date-cell">
|
||||
{{ $invoice->invoice_date }}
|
||||
</td>
|
||||
|
||||
<td class="date-cell">
|
||||
{{ $invoice->due_date }}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="{{ route('admin.invoices.edit', $invoice->id) }}"
|
||||
class="btn-entry">
|
||||
<i class="bi bi-pencil"></i> Entry
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
{{-- 1 new column वाढवलाय म्हणून colspan 11 --}}
|
||||
<td colspan="11" class="text-muted">No invoices found</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
|
||||
<td>
|
||||
<a href="{{ route('admin.invoices.edit', $invoice->id) }}"
|
||||
class="btn-entry">
|
||||
<i class="bi bi-pencil"></i> Entry
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="10" class="text-muted">No invoices found</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1253,11 +1359,14 @@
|
||||
<i class="bi bi-check-circle-fill status-icon"></i>
|
||||
@elseif($invoice->status == 'pending')
|
||||
<i class="bi bi-clock-fill status-icon"></i>
|
||||
@elseif($invoice->status == 'paying')
|
||||
<i class="bi bi-arrow-repeat status-icon"></i>
|
||||
@elseif($invoice->status == 'overdue')
|
||||
<i class="bi bi-exclamation-triangle-fill status-icon"></i>
|
||||
@endif
|
||||
{{ ucfirst($invoice->status) }}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mobile-invoice-details">
|
||||
@@ -1270,8 +1379,8 @@
|
||||
<span class="mobile-detail-value">₹{{ number_format($invoice->final_amount, 2) }}</span>
|
||||
</div>
|
||||
<div class="mobile-detail-item">
|
||||
<span class="mobile-detail-label">GST</span>
|
||||
<span class="mobile-detail-value">{{ $invoice->gst_percent }}%</span>
|
||||
<span class="mobile-detail-label">GST Amount</span>
|
||||
<span class="mobile-detail-value">₹{{ number_format($invoice->gst_amount, 2) }}</span>
|
||||
</div>
|
||||
<div class="mobile-detail-item">
|
||||
<span class="mobile-detail-label">Total</span>
|
||||
@@ -1341,7 +1450,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Pagination state
|
||||
@@ -1388,18 +1496,28 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
// Search and filter functionality for both desktop and mobile
|
||||
const searchInput = document.getElementById('invoiceSearch');
|
||||
// Function to parse dd-mm-yyyy to Date object
|
||||
function parseDate(dateStr) {
|
||||
if (!dateStr) return null;
|
||||
const parts = dateStr.split('-');
|
||||
if (parts.length === 3) {
|
||||
// dd-mm-yyyy format
|
||||
return new Date(parts[2], parts[1] - 1, parts[0]);
|
||||
}
|
||||
return new Date(dateStr);
|
||||
}
|
||||
|
||||
// Search and filter functionality
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const statusFilter = document.getElementById('statusFilter');
|
||||
const startDateInput = document.getElementById('startDate');
|
||||
const endDateInput = document.getElementById('endDate');
|
||||
const searchButton = document.getElementById('searchButton');
|
||||
const startDate = document.getElementById('startDate');
|
||||
const endDate = document.getElementById('endDate');
|
||||
|
||||
function filterInvoices() {
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
const statusValue = statusFilter.value;
|
||||
const startDate = startDateInput.value;
|
||||
const endDate = endDateInput.value;
|
||||
const startDateValue = startDate.value;
|
||||
const endDateValue = endDate.value;
|
||||
|
||||
filteredInvoices = allInvoices.filter(invoice => {
|
||||
let include = true;
|
||||
@@ -1413,18 +1531,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (statusValue && invoice.status !== statusValue) {
|
||||
if (statusValue && statusValue !== 'all' && invoice.status !== statusValue) {
|
||||
include = false;
|
||||
}
|
||||
|
||||
// Date filter
|
||||
if (startDate || endDate) {
|
||||
const invoiceDate = new Date(invoice.invoice_date);
|
||||
const start = startDate ? new Date(startDate) : null;
|
||||
const end = endDate ? new Date(endDate) : null;
|
||||
// Date range filter
|
||||
if (startDateValue || endDateValue) {
|
||||
const invoiceDate = parseDate(invoice.invoice_date);
|
||||
|
||||
if (start && invoiceDate < start) include = false;
|
||||
if (end && invoiceDate > end) include = false;
|
||||
if (startDateValue) {
|
||||
const start = parseDate(startDateValue);
|
||||
if (invoiceDate < start) include = false;
|
||||
}
|
||||
|
||||
if (endDateValue) {
|
||||
const end = parseDate(endDateValue);
|
||||
if (invoiceDate > end) include = false;
|
||||
}
|
||||
}
|
||||
|
||||
return include;
|
||||
@@ -1437,14 +1560,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Add event listeners for filtering
|
||||
searchInput.addEventListener('input', filterInvoices);
|
||||
searchButton.addEventListener('click', filterInvoices);
|
||||
statusFilter.addEventListener('change', filterInvoices);
|
||||
startDateInput.addEventListener('change', filterInvoices);
|
||||
endDateInput.addEventListener('change', filterInvoices);
|
||||
|
||||
startDate.addEventListener('change', filterInvoices);
|
||||
endDate.addEventListener('change', filterInvoices);
|
||||
|
||||
// Also trigger search on Enter key in search input
|
||||
searchInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
filterInvoices();
|
||||
}
|
||||
});
|
||||
@@ -1534,7 +1657,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const mobileContainer = document.getElementById('mobileInvoicesContainer');
|
||||
|
||||
if (filteredInvoices.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="10" class="text-muted">No invoices found</td></tr>';
|
||||
// 1 column वाढवल्यामुळे colspan 11
|
||||
tbody.innerHTML = '<tr><td colspan="11" class="text-muted">No invoices found</td></tr>';
|
||||
mobileContainer.innerHTML = '<div class="text-muted text-center py-4">No invoices found</div>';
|
||||
return;
|
||||
}
|
||||
@@ -1565,14 +1689,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</div>
|
||||
</td>
|
||||
<td class="customer-cell">${invoice.customer_name}</td>
|
||||
<!-- NEW: Container column -->
|
||||
<td class="customer-cell">
|
||||
${invoice.container ? (invoice.container.container_number ?? '—') : '—'}
|
||||
</td>
|
||||
<td class="amount-cell">₹${parseFloat(invoice.final_amount).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</td>
|
||||
<td class="gst-cell">${invoice.gst_percent}%</td>
|
||||
<td class="amount-cell">₹${parseFloat(invoice.gst_amount).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</td>
|
||||
<td class="amount-cell">₹${parseFloat(invoice.final_amount_with_gst).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</td>
|
||||
<td>
|
||||
<span class="badge badge-${invoice.status}">
|
||||
${invoice.status === 'paid' ? '<i class="bi bi-check-circle-fill status-icon"></i>' : ''}
|
||||
${invoice.status === 'pending' ? '<i class="bi bi-clock-fill status-icon"></i>' : ''}
|
||||
${invoice.status === 'overdue' ? '<i class="bi bi-exclamation-triangle-fill status-icon"></i>' : ''}
|
||||
${invoice.status === 'paying' ? '<i class="bi bi-arrow-repeat status-icon"></i>' : ''}
|
||||
${invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
|
||||
</span>
|
||||
</td>
|
||||
@@ -1606,6 +1735,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
${invoice.status === 'paid' ? '<i class="bi bi-check-circle-fill status-icon"></i>' : ''}
|
||||
${invoice.status === 'pending' ? '<i class="bi bi-clock-fill status-icon"></i>' : ''}
|
||||
${invoice.status === 'overdue' ? '<i class="bi bi-exclamation-triangle-fill status-icon"></i>' : ''}
|
||||
${invoice.status === 'paying' ? '<i class="bi bi-arrow-repeat status-icon"></i>' : ''}
|
||||
${invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -1614,13 +1744,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<span class="mobile-detail-label">Customer</span>
|
||||
<span class="mobile-detail-value">${invoice.customer_name}</span>
|
||||
</div>
|
||||
<!-- NEW: Container for mobile -->
|
||||
<div class="mobile-detail-item">
|
||||
<span class="mobile-detail-label">Container</span>
|
||||
<span class="mobile-detail-value">
|
||||
${invoice.container ? (invoice.container.container_number ?? '—') : '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mobile-detail-item">
|
||||
<span class="mobile-detail-label">Amount</span>
|
||||
<span class="mobile-detail-value">₹${parseFloat(invoice.final_amount).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</span>
|
||||
</div>
|
||||
<div class="mobile-detail-item">
|
||||
<span class="mobile-detail-label">GST</span>
|
||||
<span class="mobile-detail-value">${invoice.gst_percent}%</span>
|
||||
<span class="mobile-detail-label">GST Amount</span>
|
||||
<span class="mobile-detail-value">₹${parseFloat(invoice.gst_amount).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</span>
|
||||
</div>
|
||||
<div class="mobile-detail-item">
|
||||
<span class="mobile-detail-label">Total</span>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -264,13 +264,24 @@ header .bi-bell .badge {
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Shipments --}}
|
||||
<!-- {{-- Shipments --}}
|
||||
@can('shipment.view')
|
||||
<a href="{{ route('admin.shipments') }}" class="{{ request()->routeIs('admin.shipments') ? 'active' : '' }}">
|
||||
<i class="bi bi-truck"></i> Shipments
|
||||
</a>
|
||||
@endcan
|
||||
@endcan -->
|
||||
|
||||
|
||||
|
||||
{{-- Container NEW MENU --}}
|
||||
@can('container.view')
|
||||
<a href="{{ route('containers.index') }}" class="{{ request()->routeIs('containers.*') ? 'active' : '' }}">
|
||||
<i class="fa-solid fa-box"></i> Container
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
|
||||
|
||||
{{-- Invoice --}}
|
||||
@can('invoice.view')
|
||||
<a href="{{ route('admin.invoices.index') }}" class="{{ request()->routeIs('admin.invoices.index') ? 'active' : '' }}">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,272 +1,623 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ $invoice->invoice_number }}</title>
|
||||
<title>Invoice #{{ $invoice->invoice_number }}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', 'Roboto', Arial, sans-serif;
|
||||
background: #F7FBFC;
|
||||
color: #1A222B;
|
||||
font-family: 'DejaVu Sans', sans-serif;
|
||||
font-size: 10px;
|
||||
color: #1a202c;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
.container {
|
||||
max-width: 850px;
|
||||
margin: 24px auto 0 auto;
|
||||
padding: 18px;
|
||||
background: #fff;
|
||||
border-radius: 13px;
|
||||
box-shadow: 0 2px 14px rgba(40,105,160,0.08);
|
||||
padding: 35px 32px 18px 32px;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
border-bottom: 2px solid #E6EBF0;
|
||||
padding-bottom: 13px;
|
||||
}
|
||||
.logo-company {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.logo {
|
||||
height: 50px;
|
||||
margin-right: 13px;
|
||||
}
|
||||
.company-details {
|
||||
margin-top: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
.company-title {
|
||||
font-size: 21px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.company-sub {
|
||||
font-size: 16px;
|
||||
|
||||
/* ── TOP HEADER ── */
|
||||
.top-header {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
border-bottom: 2px solid #1a202c;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.invoice-details {
|
||||
text-align: right;
|
||||
min-width: 220px;
|
||||
}
|
||||
.invoice-title {
|
||||
font-weight: bold;
|
||||
font-size: 23px;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.paid-label {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.paid-tag {
|
||||
background: #23BF47;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
border-radius: 8px;
|
||||
padding: 4px 16px 4px 22px;
|
||||
font-size: 17px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
.paid-tag:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
top: 7px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.paid-date {
|
||||
font-size: 14px;
|
||||
color: #23BF47;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.bill-section {
|
||||
background: #F3F7FB;
|
||||
border-radius: 11px;
|
||||
padding: 20px 18px 13px 18px;
|
||||
margin: 28px 0 16px 0;
|
||||
box-shadow: 0 0px 0px #0000;
|
||||
}
|
||||
.bill-title {
|
||||
font-size: 17px;
|
||||
font-weight: bold;
|
||||
color: #23355D;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.bill-details {
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
table {
|
||||
.header-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 9px;
|
||||
margin-bottom: 13px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
th {
|
||||
background: #F6F7F9;
|
||||
padding: 10px 0;
|
||||
font-size: 15px;
|
||||
color: #6781A6;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
.header-table td {
|
||||
vertical-align: middle;
|
||||
padding: 0;
|
||||
}
|
||||
/* Logo cell — fixed width, logo fits inside */
|
||||
.logo-cell {
|
||||
width: 60px;
|
||||
}
|
||||
.logo-cell img {
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
display: block;
|
||||
}
|
||||
/* Company name cell */
|
||||
.title-cell {
|
||||
padding-left: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
td {
|
||||
padding: 7px 0;
|
||||
color: #222;
|
||||
font-size: 15px;
|
||||
border: none;
|
||||
text-align: left;
|
||||
}
|
||||
tbody tr:not(:last-child) td {
|
||||
border-bottom: 1px solid #E6EBF0;
|
||||
}
|
||||
tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
.totals-row td {
|
||||
.company-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #23355D;
|
||||
color: #1a202c;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.gst-row td {
|
||||
font-weight: 500;
|
||||
color: #23BF47;
|
||||
}
|
||||
.total-row td {
|
||||
.company-subtitle {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
font-size: 17px;
|
||||
color: #222;
|
||||
color: #2d3748;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.payment-info {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 9px;
|
||||
font-size: 15px;
|
||||
.company-tagline {
|
||||
font-size: 8px;
|
||||
color: #718096;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.ref-number {
|
||||
font-size: 14px;
|
||||
color: #6781A6;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 2px;
|
||||
|
||||
/* ── INFO TABLE (Customer | Invoice side by side) ── */
|
||||
.info-outer {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid #cbd5e0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.footer {
|
||||
border-top: 1.2px solid #E6EBF0;
|
||||
margin-top: 25px;
|
||||
padding-top: 12px;
|
||||
font-size: 16px;
|
||||
color: #888;
|
||||
.info-outer td {
|
||||
vertical-align: top;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.info-left { width: 50%; }
|
||||
.info-right { width: 50%; border-left: 1px solid #cbd5e0; }
|
||||
|
||||
.section-title {
|
||||
font-size: 8px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #718096;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding-bottom: 3px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.info-line {
|
||||
font-size: 9px;
|
||||
color: #2d3748;
|
||||
margin-bottom: 2px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── STATUS BADGE ── */
|
||||
.badge {
|
||||
display: inline;
|
||||
padding: 2px 6px;
|
||||
font-size: 8px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.badge-paid { background: #c6f6d5; color: #22543d; }
|
||||
.badge-pending { background: #fef3c7; color: #92400e; }
|
||||
.badge-paying { background: #dbeafe; color: #1e40af; }
|
||||
.badge-overdue { background: #fee2e2; color: #991b1b; }
|
||||
|
||||
/* ── BILL TO ── */
|
||||
.bill-to-box {
|
||||
border: 1px solid #cbd5e0;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 10px;
|
||||
background: #f7fafc;
|
||||
}
|
||||
.bill-to-label {
|
||||
font-size: 8px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #718096;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.bill-name { font-size: 12px; font-weight: bold; color: #1a202c; margin-bottom: 2px; }
|
||||
.bill-sub { font-size: 10px; font-weight: bold; color: #2d3748; margin-bottom: 2px; }
|
||||
.bill-detail { font-size: 9px; color: #4a5568; line-height: 1.8; }
|
||||
|
||||
/* ── SUMMARY GRID — 4 boxes per row ── */
|
||||
.summary-wrap { margin-bottom: 12px; }
|
||||
.summary-label {
|
||||
font-size: 9px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #4a5568;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.summary-outer {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.sbox {
|
||||
width: 25%;
|
||||
border: 1px solid #cbd5e0;
|
||||
padding: 7px 8px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.footer strong {
|
||||
color: #222;
|
||||
font-weight: 500;
|
||||
.sbox-row1 { background: #ffffff; }
|
||||
.sbox-row2 { background: #f7fafc; }
|
||||
.sbox-lbl {
|
||||
font-size: 7px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #718096;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.sbox-val {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
color: #1a202c;
|
||||
}
|
||||
.sbox-sub {
|
||||
font-size: 7px;
|
||||
color: #a0aec0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
/* ── CHARGE GROUPS ── */
|
||||
.cg-header {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
background: #2d3748;
|
||||
padding: 5px 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.cg-group-wrap {
|
||||
border: 1px solid #cbd5e0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.cg-sum-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 9px;
|
||||
}
|
||||
.cg-sum-table th {
|
||||
background: #edf2f7;
|
||||
padding: 5px;
|
||||
font-size: 8px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: #4a5568;
|
||||
border: 1px solid #cbd5e0;
|
||||
text-align: left;
|
||||
}
|
||||
.cg-sum-table td {
|
||||
padding: 5px;
|
||||
border: 1px solid #cbd5e0;
|
||||
color: #2d3748;
|
||||
font-size: 9px;
|
||||
}
|
||||
.cg-item-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 8px;
|
||||
}
|
||||
.cg-item-table th {
|
||||
background: #f0fff4;
|
||||
padding: 4px 5px;
|
||||
font-size: 7px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: #276749;
|
||||
border: 1px solid #c6f6d5;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cg-item-table td {
|
||||
padding: 4px 5px;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #2d3748;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.row-even { background: #f7fafc; }
|
||||
|
||||
/* ── GRAND TOTAL ── */
|
||||
.grand-outer {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.grand-spacer { width: 60%; }
|
||||
.grand-inner { width: 40%; vertical-align: top; }
|
||||
.grand-table { width: 100%; border-collapse: collapse; font-size: 9px; }
|
||||
.grand-table td { padding: 4px 8px; border: 1px solid #e2e8f0; }
|
||||
.g-lbl { text-align: right; font-weight: bold; color: #4a5568; background: #f7fafc; }
|
||||
.g-val { text-align: right; font-weight: bold; color: #1a202c; }
|
||||
.g-total-lbl { text-align: right; font-size: 11px; font-weight: bold; color: #1a202c; background: #ebf8ff; border-top: 2px solid #2b6cb0; }
|
||||
.g-total-val { text-align: right; font-size: 11px; font-weight: bold; color: #1a202c; background: #ebf8ff; border-top: 2px solid #2b6cb0; }
|
||||
|
||||
/* ── INSTALLMENTS ── */
|
||||
.install-section { margin-top: 12px; }
|
||||
.install-header {
|
||||
font-size: 9px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background: #2d3748;
|
||||
padding: 5px 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.install-table { width: 100%; border-collapse: collapse; font-size: 9px; }
|
||||
.install-table th {
|
||||
background: #edf2f7;
|
||||
padding: 5px 6px;
|
||||
font-size: 8px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: #4a5568;
|
||||
border: 1px solid #cbd5e0;
|
||||
}
|
||||
.install-table td { padding: 4px 6px; border: 1px solid #e2e8f0; color: #2d3748; }
|
||||
|
||||
/* ── HELPERS ── */
|
||||
.tr { text-align: right; }
|
||||
.tc { text-align: center; }
|
||||
.bold { font-weight: bold; }
|
||||
|
||||
/* ── FOOTER ── */
|
||||
.footer {
|
||||
margin-top: 16px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
font-size: 8px;
|
||||
color: #718096;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Header Section -->
|
||||
<div class="header">
|
||||
<div class="logo-company">
|
||||
<img src="{{ public_path('images/kent_logo2.png') }}" class="logo">
|
||||
<div class="company-details">
|
||||
<div class="company-title">{{ $invoice->company_name ?? 'Kent International Pvt. Ltd.' }}</div>
|
||||
<div class="company-sub"></div>
|
||||
{{ $invoice->company_address ?? '123 Business Park, Sector 5' }}<br>
|
||||
{{ $invoice->company_city ?? 'Gurugram, Haryana 122001' }}<br>
|
||||
{{ $invoice->company_country ?? 'India' }}<br>
|
||||
GST: {{ $invoice->company_gst ?? 'GST123456789' }}<br>
|
||||
Email: {{ $invoice->company_email ?? 'billing@kent.com' }}<br>
|
||||
Phone: {{ $invoice->company_phone ?? '+91 124 123 4567' }}
|
||||
|
||||
{{-- ══ VARIABLES ══ --}}
|
||||
@php
|
||||
$companyName = $invoice->company_name ?: ($invoice->customer->company_name ?? null);
|
||||
$custName = $invoice->customer_name;
|
||||
$custAddress = $invoice->customer_address ?: ($invoice->customer->address ?? null);
|
||||
$custPincode = $invoice->pincode ?: ($invoice->customer->pincode ?? null);
|
||||
$custEmail = $invoice->customer_email ?: ($invoice->customer->email ?? null);
|
||||
$custMobile = $invoice->customer_mobile ?: ($invoice->customer->mobile_no ?? null);
|
||||
@endphp
|
||||
|
||||
{{-- ══ 1) HEADER — Logo + Company Name ══ --}}
|
||||
<div class="top-header">
|
||||
<table class="header-table">
|
||||
<tr>
|
||||
{{-- Logo: 55x55px — DomPDF साठी public_path() वापरतो --}}
|
||||
<td class="logo-cell">
|
||||
<img src="{{ public_path('images/kent_logo2.png') }}" alt="Logo" width="55" height="55">
|
||||
</td>
|
||||
{{-- Company Name + Subtitle + Tagline --}}
|
||||
<td class="title-cell">
|
||||
<div class="company-name">KENT</div>
|
||||
<div class="company-subtitle">International Pvt. Ltd.</div>
|
||||
<div class="company-tagline">
|
||||
Address Line 1, City, State – Pincode |
|
||||
Email: info@company.com |
|
||||
Phone: +91 1234567890
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- ══ 2) CUSTOMER | INVOICE ══ --}}
|
||||
<table class="info-outer">
|
||||
<tr>
|
||||
<td class="info-left">
|
||||
<div class="section-title">Customer Details</div>
|
||||
@if($companyName)
|
||||
<div class="info-line bold" style="font-size:11px;">{{ $companyName }}</div>
|
||||
<div class="info-line">{{ $custName }}</div>
|
||||
@else
|
||||
<div class="info-line bold" style="font-size:11px;">{{ $custName }}</div>
|
||||
@endif
|
||||
@if($custAddress)<div class="info-line">{{ $custAddress }}</div>@endif
|
||||
@if($custPincode)<div class="info-line">{{ $custPincode }}</div>@endif
|
||||
@if($custEmail)<div class="info-line">Email: {{ $custEmail }}</div>@endif
|
||||
@if($custMobile)<div class="info-line">Phone: {{ $custMobile }}</div>@endif
|
||||
</td>
|
||||
<td class="info-right">
|
||||
<div class="section-title">Invoice Details</div>
|
||||
<div class="info-line"><strong>Invoice #:</strong> {{ $invoice->invoice_number }}</div>
|
||||
<div class="info-line"><strong>Date:</strong> {{ \Carbon\Carbon::parse($invoice->invoice_date)->format('d M, Y') }}</div>
|
||||
<div class="info-line"><strong>Due Date:</strong> {{ \Carbon\Carbon::parse($invoice->due_date)->format('d M, Y') }}</div>
|
||||
<div class="info-line">
|
||||
<strong>Status:</strong>
|
||||
<span class="badge badge-{{ strtolower($invoice->status) }}">{{ strtoupper($invoice->status) }}</span>
|
||||
</div>
|
||||
<div class="invoice-details">
|
||||
<div class="invoice-title">INVOICE</div>
|
||||
Invoice #: {{ $invoice->invoice_number }}<br>
|
||||
Issue Date: {{ date('d M Y', strtotime($invoice->invoice_date)) }}<br>
|
||||
Due Date: {{ date('d M Y', strtotime($invoice->due_date)) }}
|
||||
@if(strtolower($invoice->status) == 'paid')
|
||||
<div class="paid-label">
|
||||
<span class="paid-tag">Paid</span>
|
||||
</div>
|
||||
<div class="paid-date">
|
||||
Paid on: {{ date('d M Y', strtotime($invoice->paid_date ?? now())) }}
|
||||
</div>
|
||||
@else
|
||||
<div class="paid-date" style="color:#d00;">Status: {{ ucfirst($invoice->status) }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bill To Section -->
|
||||
<div class="bill-section">
|
||||
<div class="bill-title">Bill To</div>
|
||||
<div class="bill-details">
|
||||
<strong>{{ $invoice->customer_name }}</strong><br>
|
||||
{{ $invoice->customer_address }}<br>
|
||||
GST: {{ $invoice->customer_gst ?? '-' }}<br>
|
||||
Email: {{ $invoice->customer_email }}<br>
|
||||
Phone: {{ $invoice->customer_mobile }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Items Table -->
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th>Qty</th>
|
||||
<th>Rate (₹)</th>
|
||||
<th>Amount (₹)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($invoice->items as $item)
|
||||
<tr>
|
||||
<td>{{ $item->description }}</td>
|
||||
<td>{{ $item->qty }}</td>
|
||||
<td>{{ number_format($item->price, 0) }}</td>
|
||||
<td>{{ number_format($item->ttl_amount, 0) }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
<tr class="totals-row">
|
||||
<td colspan="3" style="text-align:right;">Subtotal:</td>
|
||||
<td>{{ number_format($invoice->subtotal, 0) }}</td>
|
||||
</tr>
|
||||
<tr class="gst-row">
|
||||
<td colspan="3" style="text-align:right;">GST ({{ $invoice->gst_percent }}%):</td>
|
||||
<td>{{ number_format($invoice->gst_amount, 0) }}</td>
|
||||
</tr>
|
||||
<tr class="total-row">
|
||||
<td colspan="3" style="text-align:right;">Total:</td>
|
||||
<td>{{ number_format($invoice->final_amount_with_gst, 0) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Payment Info & Reference -->
|
||||
<div class="payment-info">
|
||||
<strong>Payment Method:</strong> {{ $invoice->payment_method ?? 'Bank Transfer' }}
|
||||
</div>
|
||||
<div class="ref-number">
|
||||
Reference Number: {{ $invoice->reference_no ?? "REF123456789" }}
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<div class="footer">
|
||||
Thank you for your business!<br>
|
||||
For any queries, please contact us at <strong>{{ $invoice->company_email ?? 'billing@kent.com' }}</strong>
|
||||
</div>
|
||||
<div class="info-line"><strong>GST Type:</strong> {{ strtoupper($invoice->tax_type ?? 'GST') }}</div>
|
||||
<div class="info-line"><strong>GST %:</strong> {{ number_format($invoice->gst_percent ?? 0, 2) }}%</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{-- ══ 3) BILL TO ══ --}}
|
||||
<div class="bill-to-box">
|
||||
<div class="bill-to-label">Bill To</div>
|
||||
@if($companyName)
|
||||
<div class="bill-name">{{ $companyName }}</div>
|
||||
<div class="bill-sub">{{ $custName }}</div>
|
||||
@else
|
||||
<div class="bill-name">{{ $custName }}</div>
|
||||
@endif
|
||||
<div class="bill-detail">
|
||||
@if($custAddress){{ $custAddress }}<br>@endif
|
||||
@if($custPincode){{ $custPincode }}<br>@endif
|
||||
@if($custEmail)Email: {{ $custEmail }}<br>@endif
|
||||
@if($custMobile)Phone: {{ $custMobile }}@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ══ 4) 8 SUMMARY BOXES ══ --}}
|
||||
@php
|
||||
$allGroupItems = $invoice->chargeGroups->flatMap(fn($g) => $g->items);
|
||||
$invoiceItemIds = $allGroupItems->pluck('invoice_item_id')->unique()->values();
|
||||
$invoiceItems = $invoice->items->whereIn('id', $invoiceItemIds);
|
||||
$totalMarkCount = $invoiceItems->pluck('mark_no')->filter()->unique()->count();
|
||||
$totalCtn = $invoiceItems->sum('ctn');
|
||||
$totalQty = $invoiceItems->sum('ttl_qty');
|
||||
$totalCbm = $invoiceItems->sum('ttl_cbm');
|
||||
$totalKg = $invoiceItems->sum('ttl_kg');
|
||||
@endphp
|
||||
|
||||
<div class="summary-wrap">
|
||||
<div class="summary-label">Container & Shipment Summary</div>
|
||||
<table class="summary-outer">
|
||||
<tr>
|
||||
<td class="sbox sbox-row1">
|
||||
<div class="sbox-lbl">Container Name</div>
|
||||
<div class="sbox-val">{{ $invoice->container->container_name ?? '—' }}</div>
|
||||
</td>
|
||||
<td class="sbox sbox-row1">
|
||||
<div class="sbox-lbl">Container Date</div>
|
||||
<div class="sbox-val">
|
||||
@if($invoice->container && $invoice->container->container_date)
|
||||
{{ \Carbon\Carbon::parse($invoice->container->container_date)->format('d M, Y') }}
|
||||
@else—@endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="sbox sbox-row1">
|
||||
<div class="sbox-lbl">Container No.</div>
|
||||
<div class="sbox-val">{{ $invoice->container->container_number ?? '—' }}</div>
|
||||
</td>
|
||||
<td class="sbox sbox-row1">
|
||||
<div class="sbox-lbl">Total Mark No.</div>
|
||||
<div class="sbox-val">{{ $totalMarkCount }}</div>
|
||||
<div class="sbox-sub">Unique marks</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sbox sbox-row2">
|
||||
<div class="sbox-lbl">Total CTN</div>
|
||||
<div class="sbox-val">{{ number_format($totalCtn, 0) }}</div>
|
||||
<div class="sbox-sub">Cartons</div>
|
||||
</td>
|
||||
<td class="sbox sbox-row2">
|
||||
<div class="sbox-lbl">Total QTY</div>
|
||||
<div class="sbox-val">{{ number_format($totalQty, 0) }}</div>
|
||||
<div class="sbox-sub">Pieces</div>
|
||||
</td>
|
||||
<td class="sbox sbox-row2">
|
||||
<div class="sbox-lbl">Total CBM</div>
|
||||
<div class="sbox-val">{{ number_format($totalCbm, 3) }}</div>
|
||||
<div class="sbox-sub">Cubic Meter</div>
|
||||
</td>
|
||||
<td class="sbox sbox-row2">
|
||||
<div class="sbox-lbl">Total KG</div>
|
||||
<div class="sbox-val">{{ number_format($totalKg, 2) }}</div>
|
||||
<div class="sbox-sub">Kilograms</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- ══ 5) CHARGE GROUPS ══ --}}
|
||||
@if($invoice->chargeGroups && $invoice->chargeGroups->count() > 0)
|
||||
<div style="margin-bottom:14px;">
|
||||
<div class="cg-header">Charge Groups</div>
|
||||
|
||||
@foreach($invoice->chargeGroups as $group)
|
||||
@php
|
||||
$groupItemIds = $group->items->pluck('invoice_item_id')->toArray();
|
||||
$groupInvItems = $invoice->items->whereIn('id', $groupItemIds);
|
||||
@endphp
|
||||
|
||||
<div class="cg-group-wrap">
|
||||
<table class="cg-sum-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:22%;">Group Name</th>
|
||||
<th style="width:11%;">Basis</th>
|
||||
<th style="width:11%;">Basis Value</th>
|
||||
<th style="width:10%;">Rate</th>
|
||||
<th style="width:13%;" class="tr">Total Charge</th>
|
||||
<th style="width:8%;">GST %</th>
|
||||
<th style="width:9%;">Tax Type</th>
|
||||
<th style="width:16%;" class="tr">Total With GST</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>{{ $group->group_name ?? 'Group '.$group->id }}</strong></td>
|
||||
<td>{{ strtoupper($group->basis_type) }}</td>
|
||||
<td>{{ number_format($group->basis_value, 3) }}</td>
|
||||
<td>{{ number_format($group->rate, 2) }}</td>
|
||||
<td class="tr">₹{{ number_format($group->total_charge, 2) }}</td>
|
||||
<td>{{ number_format($group->gst_percent ?? 0, 2) }}%</td>
|
||||
<td>{{ strtoupper($group->tax_type ?? 'GST') }}</td>
|
||||
<td class="tr"><strong>₹{{ number_format($group->total_with_gst, 2) }}</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if($groupInvItems->count() > 0)
|
||||
<table class="cg-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:3%;">#</th>
|
||||
<th style="width:15%;">Description</th>
|
||||
<th style="width:7%;">Mark No</th>
|
||||
<th style="width:5%;" class="tc">QTY</th>
|
||||
<th style="width:6%;" class="tr">TTL QTY</th>
|
||||
<th style="width:6%;" class="tr">CBM</th>
|
||||
<th style="width:6%;" class="tr">TTL CBM</th>
|
||||
<th style="width:5%;" class="tr">KG</th>
|
||||
<th style="width:6%;" class="tr">TTL KG</th>
|
||||
<th style="width:7%;" class="tr">TTL Amt</th>
|
||||
<th style="width:5%;" class="tr">Rate</th>
|
||||
<th style="width:7%;" class="tr">Total Charge</th>
|
||||
<th style="width:5%;" class="tr">GST %</th>
|
||||
<th style="width:6%;" class="tr">Item GST</th>
|
||||
<th style="width:6%;" class="tr">Item Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($groupInvItems as $idx => $item)
|
||||
@php
|
||||
$groupBasisTotal = $groupInvItems->sum(function($i) use ($group) {
|
||||
return match($group->basis_type) {
|
||||
'ttl_qty' => $i->ttl_qty ?? 0,
|
||||
'ttl_cbm' => $i->ttl_cbm ?? 0,
|
||||
'ttl_kg' => $i->ttl_kg ?? 0,
|
||||
'amount' => $i->ttl_amount ?? 0,
|
||||
default => $i->ttl_amount ?? 0,
|
||||
};
|
||||
});
|
||||
$itemBasisValue = match($group->basis_type) {
|
||||
'ttl_qty' => $item->ttl_qty ?? 0,
|
||||
'ttl_cbm' => $item->ttl_cbm ?? 0,
|
||||
'ttl_kg' => $item->ttl_kg ?? 0,
|
||||
'amount' => $item->ttl_amount ?? 0,
|
||||
default => $item->ttl_amount ?? 0,
|
||||
};
|
||||
$itemCharge = $groupBasisTotal > 0
|
||||
? ($itemBasisValue / $groupBasisTotal) * $group->total_charge
|
||||
: 0;
|
||||
$gstPct = $group->gst_percent ?? 0;
|
||||
$itemGst = $itemCharge * $gstPct / 100;
|
||||
$itemTotal = $itemCharge + $itemGst;
|
||||
@endphp
|
||||
<tr class="{{ $idx % 2 === 1 ? 'row-even' : '' }}">
|
||||
<td class="tc">{{ $idx + 1 }}</td>
|
||||
<td>{{ $item->description }}</td>
|
||||
<td>{{ $item->mark_no ?? '—' }}</td>
|
||||
<td class="tc">{{ $item->qty ?? 0 }}</td>
|
||||
<td class="tr">{{ number_format($item->ttl_qty ?? 0, 0) }}</td>
|
||||
<td class="tr">{{ number_format($item->cbm ?? 0, 3) }}</td>
|
||||
<td class="tr">{{ number_format($item->ttl_cbm ?? 0, 3) }}</td>
|
||||
<td class="tr">{{ number_format($item->kg ?? 0, 2) }}</td>
|
||||
<td class="tr">{{ number_format($item->ttl_kg ?? 0, 2) }}</td>
|
||||
<td class="tr">₹{{ number_format($item->ttl_amount ?? 0, 2) }}</td>
|
||||
<td class="tr">{{ number_format($group->rate, 2) }}</td>
|
||||
<td class="tr">₹{{ number_format($itemCharge, 2) }}</td>
|
||||
<td class="tr">{{ number_format($gstPct, 2) }}%</td>
|
||||
<td class="tr">₹{{ number_format($itemGst, 2) }}</td>
|
||||
<td class="tr"><strong>₹{{ number_format($itemTotal, 2) }}</strong></td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ══ GRAND TOTAL ══ --}}
|
||||
<table class="grand-outer">
|
||||
<tr>
|
||||
<td class="grand-spacer"></td>
|
||||
<td class="grand-inner">
|
||||
<table class="grand-table">
|
||||
<tr>
|
||||
<td class="g-lbl">Charge Groups Total:</td>
|
||||
<td class="g-val">₹{{ number_format($invoice->charge_groups_total ?? 0, 2) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="g-lbl">GST Amount ({{ number_format($invoice->gst_percent ?? 0, 2) }}%):</td>
|
||||
<td class="g-val">₹{{ number_format($invoice->gst_amount ?? 0, 2) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="g-total-lbl">Grand Total:</td>
|
||||
<td class="g-total-val">₹{{ number_format($invoice->grand_total_with_charges ?? 0, 2) }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{-- ══ INSTALLMENTS ══ --}}
|
||||
@if($invoice->installments && $invoice->installments->count() > 0)
|
||||
<div class="install-section">
|
||||
<div class="install-header">Payment Installments</div>
|
||||
<table class="install-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:6%;" class="tc">#</th>
|
||||
<th style="width:20%;">Date</th>
|
||||
<th style="width:25%;">Payment Method</th>
|
||||
<th style="width:25%;">Reference No</th>
|
||||
<th style="width:24%;" class="tr">Amount (₹)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($invoice->installments as $idx => $inst)
|
||||
<tr>
|
||||
<td class="tc">{{ $idx + 1 }}</td>
|
||||
<td>{{ \Carbon\Carbon::parse($inst->installment_date)->format('d M, Y') }}</td>
|
||||
<td>{{ strtoupper($inst->payment_method) }}</td>
|
||||
<td>{{ $inst->reference_no ?? '—' }}</td>
|
||||
<td class="tr">₹{{ number_format($inst->amount, 2) }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@php
|
||||
$totalPaid = $invoice->installments->sum('amount');
|
||||
$grandTotal = $invoice->grand_total_with_charges ?? 0;
|
||||
$remaining = max(0, $grandTotal - $totalPaid);
|
||||
@endphp
|
||||
|
||||
<table class="grand-outer" style="margin-top:6px;">
|
||||
<tr>
|
||||
<td class="grand-spacer"></td>
|
||||
<td class="grand-inner">
|
||||
<table class="grand-table">
|
||||
<tr>
|
||||
<td class="g-lbl" style="color:#059669;">Total Paid:</td>
|
||||
<td class="g-val" style="color:#059669;">₹{{ number_format($totalPaid, 2) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="g-lbl" style="color:#dc2626;">Remaining:</td>
|
||||
<td class="g-val" style="color:#dc2626;">₹{{ number_format($remaining, 2) }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ══ FOOTER ══ --}}
|
||||
<div class="footer">
|
||||
Thank you for your business! | For queries: info@company.com | +91 1234567890
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
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>
|
||||
252
resources/views/admin/pdf/orderpdf.blade.php
Normal file
252
resources/views/admin/pdf/orderpdf.blade.php
Normal file
@@ -0,0 +1,252 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Invoices Report</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'DejaVu Sans', Arial, sans-serif;
|
||||
font-size: 10px;
|
||||
color: #1f2937;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 3px solid #3b82f6;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
color: #1f2937;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.header .date {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: table;
|
||||
width: 100%;
|
||||
margin-bottom: 25px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
display: table-cell;
|
||||
width: 25%;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
border: 2px solid #e5e7eb;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.stat-box.total {
|
||||
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
|
||||
.stat-box.paid {
|
||||
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
|
||||
border-left: 4px solid #22c55e;
|
||||
}
|
||||
|
||||
.stat-box.pending {
|
||||
background: linear-gradient(135deg, #fffbeb 0%, #fef9c3 100%);
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
|
||||
.stat-box.overdue {
|
||||
background: linear-gradient(135deg, #fff5f5 0%, #fee2e2 100%);
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-box.total .stat-value { color: #3b82f6; }
|
||||
.stat-box.paid .stat-value { color: #16a34a; }
|
||||
.stat-box.pending .stat-value { color: #d97706; }
|
||||
.stat-box.overdue .stat-value { color: #dc2626; }
|
||||
|
||||
.stat-label {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
table.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
table.data-table thead {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
table.data-table th {
|
||||
padding: 10px 8px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 9px;
|
||||
border: 1px solid #5a67d8;
|
||||
}
|
||||
|
||||
table.data-table td {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
border: 1px solid #e5e7eb;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
table.data-table tbody tr:nth-child(even) {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
table.data-table tbody tr:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.status-paid {
|
||||
background: linear-gradient(135deg, #10b981, #34d399);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: linear-gradient(135deg, #fef3c7, #fde68a);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.status-overdue {
|
||||
background: linear-gradient(135deg, #fef2f2, #fecaca);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
font-size: 9px;
|
||||
color: #9ca3af;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>📊 Invoices Report</h1>
|
||||
<div class="date">Generated on: {{ now()->format('d M Y, h:i A') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Boxes -->
|
||||
<div class="stats-grid">
|
||||
<div class="stats-row">
|
||||
<div class="stat-box total">
|
||||
<div class="stat-value">{{ $totalInvoices }}</div>
|
||||
<div class="stat-label">Total Invoices</div>
|
||||
</div>
|
||||
<div class="stat-box paid">
|
||||
<div class="stat-value">{{ $paidInvoices }}</div>
|
||||
<div class="stat-label">Paid Invoices</div>
|
||||
</div>
|
||||
<div class="stat-box pending">
|
||||
<div class="stat-value">{{ $pendingInvoices }}</div>
|
||||
<div class="stat-label">Pending Invoices</div>
|
||||
</div>
|
||||
<div class="stat-box overdue">
|
||||
<div class="stat-value">{{ $overdueInvoices }}</div>
|
||||
<div class="stat-label">Overdue Invoices</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
@if($invoices->count() > 0)
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Invoice No</th>
|
||||
<th>Date</th>
|
||||
<th>Mark No</th>
|
||||
<th>Container</th>
|
||||
<th>Cont. 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) : '-' }}</td>
|
||||
<td>{{ $inv->final_amount_with_gst ? '₹'.number_format($inv->final_amount_with_gst, 2) : '-' }}</td>
|
||||
<td>
|
||||
@php
|
||||
$status = strtolower($inv->invoice_status ?? 'pending');
|
||||
@endphp
|
||||
<span class="status-badge status-{{ $status }}">{{ ucfirst($status) }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@else
|
||||
<div class="no-data">
|
||||
No invoices found matching the criteria.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="footer">
|
||||
Kent Logistics - Invoice Management System | Page 1 of 1
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user