Compare commits

...

17 Commits

Author SHA256 Message Date
Utkarsh Khedkar
0257b68f16 Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2026-03-13 23:06:51 +05:30
Utkarsh Khedkar
785f2564be changes 2026-03-13 23:06:19 +05:30
Abhishek Mali
9a6ca49ad7 user order controller update for show view data 2026-03-13 20:19:34 +05:30
Abhishek Mali
bf2689e62d container and invoice api done small api like view invoice traking in order section is remaning 2026-03-13 17:25:58 +05:30
Utkarsh Khedkar
c25b468c77 Status Updated paying 2026-03-12 18:20:09 +05:30
Utkarsh Khedkar
5d8a746876 Status Updated 2026-03-12 18:11:43 +05:30
Utkarsh Khedkar
bb2a361a97 Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2026-03-12 12:34:49 +05:30
Utkarsh Khedkar
6b5876e08f Fetch Data For Customer Details 2026-03-12 12:34:27 +05:30
Abhishek Mali
43b1a64911 ajax update 2026-03-12 11:48:42 +05:30
Utkarsh Khedkar
ff4c006ca4 Gst Updates 2026-03-11 20:02:43 +05:30
Utkarsh Khedkar
d5e9113820 Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2026-03-09 12:28:11 +05:30
Utkarsh Khedkar
bddbcf5c5f logo changes 2026-03-09 12:27:42 +05:30
Abhishek Mali
0c51ed1489 mark list and custumer data update 2026-03-09 12:04:08 +05:30
Utkarsh Khedkar
9cc6959396 Pdf Changes Done 2026-03-09 10:24:44 +05:30
Utkarsh Khedkar
c11467068c Frontend Changes 2026-02-28 11:00:48 +05:30
Abhishek Mali
599023166a update staff permissions 2026-02-27 12:59:20 +05:30
Utkarsh Khedkar
e188780329 All Kent Code Updated 2026-02-27 10:51:26 +05:30
84 changed files with 13531 additions and 7399 deletions

View File

@@ -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

View 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'));
}
}

View File

@@ -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'));
}
}

View File

@@ -3,164 +3,237 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use Mpdf\Mpdf;
use App\Models\InvoiceInstallment;
use App\Models\InvoiceChargeGroup;
use App\Models\InvoiceChargeGroupItem;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Mpdf\Mpdf;
class AdminInvoiceController extends Controller
{
// -------------------------------------------------------------
// INVOICE LIST PAGE
// -------------------------------------------------------------
public function index()
public function index(Request $request)
{
$invoices = Invoice::with(['order.shipments'])->latest()->get();
$query = Invoice::with(['items', 'customer', 'container']);
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', 'installments'])->findOrFail($id);
{
$invoice = Invoice::with([
'items',
'chargeGroups.items',
])->findOrFail($id);
$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'));
}
// -------------------------------------------------------------
// EDIT INVOICE PAGE
// -------------------------------------------------------------
public function edit($id)
{
$invoice = Invoice::with(['order.shipments'])->findOrFail($id);
$shipment = $invoice->order?->shipments?->first();
return view('admin.invoice_edit', compact('invoice', 'shipment'));
$invoice = Invoice::with([
'items',
'customer',
'container',
'chargeGroups.items',
'installments',
])->findOrFail($id);
// ✅ Customer details sync — जर test data आला असेल तर fix होईल
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')
->with('success', 'Invoice updated & PDF generated successfully.');
->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
// -------------------------------------------------------------
public function generateInvoicePDF($invoice)
{
$invoice->load(['items', 'order.shipments']);
$shipment = $invoice->order?->shipments?->first();
$invoice->load(['items', 'customer', 'container']);
$shipment = null;
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
$folder = public_path('invoices/');
$folder = public_path('invoices/');
if (!file_exists($folder)) {
mkdir($folder, 0777, true);
}
$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]);
}
public function downloadInvoice($id)
{
$invoice = Invoice::findOrFail($id);
// Generate PDF if missing
if (
!$invoice->pdf_path ||
!file_exists(public_path($invoice->pdf_path))
) {
$this->generateInvoicePDF($invoice);
$invoice->refresh();
}
return response()->download(public_path($invoice->pdf_path));
}
// -------------------------------------------------------------
// INSTALLMENTS (ADD/DELETE)
// INSTALLMENTS (ADD)
// -------------------------------------------------------------
public function storeInstallment(Request $request, $invoice_id)
{
@@ -173,14 +246,15 @@ class AdminInvoiceController extends Controller
$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.'
'status' => 'error',
'message' => 'Installment amount exceeds remaining balance.',
], 422);
}
@@ -192,54 +266,202 @@ class AdminInvoiceController extends Controller
'amount' => $request->amount,
]);
$newPaid = $paidTotal + $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']);
$this->generateInvoicePDF($invoice);
}
// Full payment logic (जर पूर्ण भरले तर status paid करणे, नाहीतर pending राहील)
// if ($newPaid >= $grandTotal && $grandTotal > 0) {
// $invoice->update([
// 'payment_method' => $request->payment_method,
// 'reference_no' => $request->reference_no,
// 'status' => ($newPaid >= $grandTotal && $grandTotal > 0) ? 'paid' : $invoice->status,
// ]);
// }
// Partial payment status logic:
$invoice->update([
'payment_method' => $request->payment_method,
'reference_no' => $request->reference_no,
'status' => ($newPaid >= $grandTotal && $grandTotal > 0) ? 'paid' : $invoice->status,
]);
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,
'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']);
$invoice = $installment->invoice;
$this->generateInvoicePDF($invoice);
$installment->delete();
$invoice->refresh();
$grandTotal = $invoice->grand_total_with_charges ?? 0;
$paidTotal = $invoice->installments()->sum('amount');
$remaining = max(0, $grandTotal - $paidTotal);
if ($paidTotal <= 0 && $grandTotal > 0) {
$invoice->update(['status' => 'pending']);
} elseif ($paidTotal > 0 && $paidTotal < $grandTotal) {
$invoice->update(['status' => 'pending']);
}
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,
'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);
// duplicate name check
$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;
}
// 1) Group create
$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,
]);
// 2) Items link
foreach ($data['itemids'] as $itemId) {
InvoiceChargeGroupItem::create([
'group_id' => $group->id,
'invoice_item_id' => $itemId,
]);
}
// 3) सर्व groups वरून invoice level totals
$invoice->load('chargeGroups');
$chargeGroupsBase = $invoice->chargeGroups->sum('total_charge'); // base
$chargeGroupsWithG = $invoice->chargeGroups->sum('total_with_gst'); // base + gst
$chargeGroupsGst = $chargeGroupsWithG - $chargeGroupsBase; // gst only
$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;
}
// 🔴 इथे main fix:
// final_amount = base (total_charge sum)
// final_amount_with_gst = base + gst (total_with_gst sum)
// grand_total_with_charges = final_amount_with_gst (same)
$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,
'gst_percent' => $invoiceGstPercent,
'tax_type' => $invoiceTaxType,
'cgst_percent' => $cgstPercent,
'sgst_percent' => $sgstPercent,
'igst_percent' => $igstPercent,
'final_amount' => $invoice->final_amount,
'final_amount_with_gst' => $invoice->final_amount_with_gst,
'grand_total_with_charges'=> $invoice->grand_total_with_charges,
]);
return response()->json([
'success' => true,
'message' => 'Charge group saved successfully.',
'group_id' => $group->id,
]);
}
}

View File

@@ -10,26 +10,118 @@ use App\Models\MarkList;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\User;
use App\Models\Container;
use App\Models\Admin;
use App\Models\Shipment;
use PDF;
use Maatwebsite\Excel\Facades\Excel;
use App\Exports\OrdersExport;
use App\Imports\OrderItemsPreviewImport;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Facades\DB;
use App\Exports\InvoicesExport;
class AdminOrderController extends Controller
{
/* ---------------------------
* LIST / DASHBOARD
* DASHBOARD (old UI: stats + recent orders)
* ---------------------------*/
public function index()
public function dashboard()
{
$orders = Order::latest()->get();
$markList = MarkList::where('status', 'active')->get();
// ── Order counts (from Invoice Management / Orders table) ──
$totalOrders = Order::count();
return view('admin.dashboard', compact('orders', 'markList'));
// "Pending" म्हणजे delivered नसलेले सर्व orders
// Order तयार होतो तेव्हा status = 'order_placed' असतो, 'pending' नाही
$deliveredStatuses = ['delivered'];
$pendingOrders = Order::whereNotIn('status', $deliveredStatuses)->count();
// ── Invoice counts ──
$totalContainers = Container::count();
$totalInvoices = Invoice::count();
$paidInvoices = Invoice::where('status', 'paid')->count();
$pendingInvoices = Invoice::where('status', 'pending')->count();
$overdueInvoices = Invoice::where('status', 'overdue')->count();
$totalRevenue = Invoice::sum('final_amount_with_gst');
// ── User / Staff counts ──
$activeCustomers = User::where('status', 'active')->count();
$inactiveCustomers = User::where('status', 'inactive')->count();
$totalStaff = Admin::where('type', 'staff')->count();
$markList = MarkList::where('status', 'active')->get();
$orders = Order::latest()->get();
return view('admin.dashboard', compact(
'totalOrders',
'pendingOrders',
'totalContainers',
'totalInvoices',
'paidInvoices',
'pendingInvoices',
'overdueInvoices',
'totalRevenue',
'activeCustomers',
'inactiveCustomers',
'totalStaff',
'orders',
'markList'
));
}
/* ---------------------------
* LIST (new: Invoices Management for Orders page)
* ---------------------------*/
public function index(Request $request)
{
$invoices = DB::table('invoices')
->leftJoin('containers', 'containers.id', '=', 'invoices.container_id')
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
->select(
'invoices.id',
'invoices.invoice_number',
'invoices.invoice_date',
'invoices.final_amount',
'invoices.final_amount_with_gst',
'invoices.status as invoice_status',
'invoices.mark_no',
'invoices.container_id', // <<< हे नक्की घाल
'containers.container_number',
'containers.container_date',
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name')
)
->when($request->filled('search'), function ($q) use ($request) {
$search = trim($request->search);
$q->where(function ($qq) use ($search) {
$qq->where('invoices.invoice_number', 'like', "%{$search}%")
->orWhere('containers.container_number', 'like', "%{$search}%")
->orWhere('invoices.mark_no', 'like', "%{$search}%")
->orWhere('mark_list.company_name', 'like', "%{$search}%")
->orWhere('mark_list.customer_name', 'like', "%{$search}%");
});
})
->when($request->filled('status'), function ($q) use ($request) {
$q->where('invoices.status', $request->status);
})
->orderByDesc('invoices.invoice_date')
->orderByDesc('invoices.id')
->get();
// ── Real DB counts (filter-independent) ──
$totalInvoices = \App\Models\Invoice::count();
$paidInvoices = \App\Models\Invoice::where('status', 'paid')->count();
$pendingInvoices = \App\Models\Invoice::where('status', 'pending')->count();
$overdueInvoices = \App\Models\Invoice::where('status', 'overdue')->count();
return view('admin.orders', compact(
'invoices',
'totalInvoices',
'paidInvoices',
'pendingInvoices',
'overdueInvoices'
));
}
/* ---------------------------
@@ -68,42 +160,39 @@ class AdminOrderController extends Controller
* ORDER ITEM MANAGEMENT (existing orders)
* ---------------------------*/
public function addItem(Request $request, $orderId)
{
$order = Order::findOrFail($orderId);
{
$order = Order::findOrFail($orderId);
$data = $request->validate([
'description' => 'required|string',
'ctn' => 'nullable|numeric',
'qty' => 'nullable|numeric',
'unit' => 'nullable|string',
'price' => 'nullable|numeric',
'cbm' => 'nullable|numeric',
'kg' => 'nullable|numeric',
'shop_no' => 'nullable|string',
]);
$data = $request->validate([
'description' => 'required|string',
'ctn' => 'nullable|numeric',
'qty' => 'nullable|numeric',
'unit' => 'nullable|string',
'price' => 'nullable|numeric',
'cbm' => 'nullable|numeric',
'kg' => 'nullable|numeric',
'shop_no' => 'nullable|string',
]);
// ✅ BACKEND CALCULATION
$ctn = (float) ($data['ctn'] ?? 0);
$qty = (float) ($data['qty'] ?? 0);
$price = (float) ($data['price'] ?? 0);
$cbm = (float) ($data['cbm'] ?? 0);
$kg = (float) ($data['kg'] ?? 0);
$ctn = (float) ($data['ctn'] ?? 0);
$qty = (float) ($data['qty'] ?? 0);
$price = (float) ($data['price'] ?? 0);
$cbm = (float) ($data['cbm'] ?? 0);
$kg = (float) ($data['kg'] ?? 0);
$data['ttl_qty'] = $ctn * $qty;
$data['ttl_amount'] = $data['ttl_qty'] * $price;
$data['ttl_cbm'] = $cbm * $ctn;
$data['ttl_kg'] = $ctn * $kg;
$data['ttl_qty'] = $ctn * $qty;
$data['ttl_amount'] = $data['ttl_qty'] * $price;
$data['ttl_cbm'] = $cbm * $ctn;
$data['ttl_kg'] = $ctn * $kg;
$data['order_id'] = $order->id;
$data['order_id'] = $order->id;
OrderItem::create($data);
OrderItem::create($data);
$this->recalcTotals($order);
$this->updateInvoiceFromOrder($order);
return redirect()->back()->with('success', 'Item added and totals updated.');
}
$this->recalcTotals($order);
return redirect()->back()->with('success', 'Item added and totals updated.');
}
public function deleteItem($id)
{
@@ -113,7 +202,6 @@ class AdminOrderController extends Controller
$item->delete();
$this->recalcTotals($order);
$this->updateInvoiceFromOrder($order);
return redirect()->back()->with('success', 'Item deleted and totals updated.');
}
@@ -126,7 +214,6 @@ class AdminOrderController extends Controller
$item->restore();
$this->recalcTotals($order);
$this->updateInvoiceFromOrder($order);
return redirect()->back()->with('success', 'Item restored and totals updated.');
}
@@ -175,14 +262,14 @@ class AdminOrderController extends Controller
$items = $order->items()->get();
$order->update([
'ctn' => (int) $items->sum(fn($i) => (int) ($i->ctn ?? 0)),
'qty' => (int) $items->sum(fn($i) => (int) ($i->qty ?? 0)),
'ttl_qty' => (int) $items->sum(fn($i) => (int) ($i->ttl_qty ?? 0)),
'ttl_amount' => (float) $items->sum(fn($i) => (float) ($i->ttl_amount ?? 0)),
'cbm' => (float) $items->sum(fn($i) => (float) ($i->cbm ?? 0)),
'ttl_cbm' => (float) $items->sum(fn($i) => (float) ($i->ttl_cbm ?? 0)),
'kg' => (float) $items->sum(fn($i) => (float) ($i->kg ?? 0)),
'ttl_kg' => (float) $items->sum(fn($i) => (float) ($i->ttl_kg ?? 0)),
'ctn' => (int) $items->sum(fn($i) => (int) ($i->ctn ?? 0)),
'qty' => (int) $items->sum(fn($i) => (int) ($i->qty ?? 0)),
'ttl_qty' => (int) $items->sum(fn($i) => (int) ($i->ttl_qty ?? 0)),
'ttl_amount'=> (float) $items->sum(fn($i) => (float) ($i->ttl_amount ?? 0)),
'cbm' => (float) $items->sum(fn($i) => (float) ($i->cbm ?? 0)),
'ttl_cbm' => (float) $items->sum(fn($i) => (float) ($i->ttl_cbm ?? 0)),
'kg' => (float) $items->sum(fn($i) => (float) ($i->kg ?? 0)),
'ttl_kg' => (float) $items->sum(fn($i) => (float) ($i->ttl_kg ?? 0)),
]);
}
@@ -285,7 +372,6 @@ class AdminOrderController extends Controller
$order = Order::with([
'markList',
'items',
'invoice.items',
'shipments' => function ($q) use ($id) {
$q->whereHas('orders', function ($oq) use ($id) {
$oq->where('orders.id', $id)
@@ -304,14 +390,14 @@ class AdminOrderController extends Controller
'order_id' => $order->order_id,
'status' => $order->status,
'totals' => [
'ctn' => $order->ctn,
'qty' => $order->qty,
'ttl_qty' => $order->ttl_qty,
'cbm' => $order->cbm,
'ttl_cbm' => $order->ttl_cbm,
'kg' => $order->kg,
'ttl_kg' => $order->ttl_kg,
'amount' => $order->ttl_amount,
'ctn' => $order->ctn,
'qty' => $order->qty,
'ttl_qty' => $order->ttl_qty,
'cbm' => $order->cbm,
'ttl_cbm' => $order->ttl_cbm,
'kg' => $order->kg,
'ttl_kg' => $order->ttl_kg,
'amount' => $order->ttl_amount,
],
'items' => $order->items,
];
@@ -365,29 +451,6 @@ class AdminOrderController extends Controller
}
$invoiceData = null;
if ($order->invoice) {
$invoice = $order->invoice;
$invoiceData = [
'invoice_no' => $invoice->invoice_number,
'status' => $invoice->status,
'invoice_date' => $invoice->invoice_date,
'due_date' => $invoice->due_date,
'customer' => [
'name' => $invoice->customer_name,
'mobile' => $invoice->customer_mobile,
'email' => $invoice->customer_email,
'address' => $invoice->customer_address,
'pincode' => $invoice->pincode,
],
'items' => $invoice->items,
'summary' => [
'amount' => $invoice->final_amount,
'cgst' => 0,
'sgst' => 0,
'total' => $invoice->final_amount_with_gst,
],
];
}
return view('admin.see_order', compact(
'order',
@@ -398,14 +461,13 @@ class AdminOrderController extends Controller
}
/* ---------------------------
* FILTERED LIST + EXPORTS
* FILTERED LIST + EXPORTS (old orders listing)
* ---------------------------*/
public function orderShow()
{
$orders = Order::with([
'markList',
'shipments',
'invoice'
])->latest('id')->get();
return view('admin.orders', compact('orders'));
@@ -414,7 +476,7 @@ class AdminOrderController extends Controller
private function buildOrdersQueryFromRequest(Request $request)
{
$query = Order::query()
->with(['markList', 'invoice', 'shipments']);
->with(['markList', 'shipments']);
if ($request->filled('search')) {
$search = trim($request->search);
@@ -427,23 +489,12 @@ class AdminOrderController extends Controller
->orWhere('origin', 'like', "%{$search}%")
->orWhere('destination', 'like', "%{$search}%");
})
->orWhereHas('invoice', function ($q3) use ($search) {
$q3->where('invoice_number', 'like', "%{$search}%");
})
->orWhereHas('shipments', function ($q4) use ($search) {
$q4->where('shipments.shipment_id', 'like', "%{$search}%");
});
});
}
if ($request->filled('status')) {
$query->where(function ($q) use ($request) {
$q->whereHas('invoice', function ($q2) use ($request) {
$q2->where('status', $request->status);
})->orWhereDoesntHave('invoice');
});
}
if ($request->filled('shipment')) {
$query->where(function ($q) use ($request) {
$q->whereHas('shipments', function ($q2) use ($request) {
@@ -465,62 +516,83 @@ class AdminOrderController extends Controller
public function downloadPdf(Request $request)
{
$orders = $this->buildOrdersQueryFromRequest($request)->get();
$filters = [
'search' => $request->search,
'status' => $request->status,
'shipment' => $request->shipment,
'from' => $request->from_date,
'to' => $request->to_date,
];
$pdf = PDF::loadView('admin.orders.pdf', compact('orders', 'filters'))
$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();
$pdf = PDF::loadView('admin.pdf.invoices_report', compact('invoices'))
->setPaper('a4', 'landscape');
return $pdf->download(
'orders-report-' . now()->format('Y-m-d') . '.pdf'
'invoices-report-' . now()->format('Y-m-d') . '.pdf'
);
}
public function downloadExcel(Request $request)
{
return Excel::download(
new OrdersExport($request),
'orders-report-' . now()->format('Y-m-d') . '.xlsx'
new InvoicesExport($request),
'invoices-report-' . now()->format('Y-m-d') . '.xlsx'
);
}
/* --------------------------------------------------
* NEW: Create Order + Invoice directly from popup
* route: admin.orders.temp.add (Create New Order form)
* --------------------------------------------------*/
public function addTempItem(Request $request)
{
// 1) order-level fields
$request->validate([
'mark_no' => 'required',
'origin' => 'required',
'destination' => 'required',
'origin' => 'nullable',
'destination' => 'nullable',
]);
// 2) multi-row items
$items = $request->validate([
'items' => 'required|array',
'items.*.description' => 'required|string',
'items.*.ctn' => 'nullable|numeric',
'items.*.qty' => 'nullable|numeric',
'items.*.unit' => 'nullable|string',
'items.*.price' => 'nullable|numeric',
'items.*.cbm' => 'nullable|numeric',
'items.*.kg' => 'nullable|numeric',
'items.*.shop_no' => 'nullable|string',
])['items'];
// रिकामे rows काढा
$items = array_filter($items, function ($row) {
return trim($row['description'] ?? '') !== '';
});
@@ -529,38 +601,31 @@ class AdminOrderController extends Controller
return back()->with('error', 'Add at least one item.');
}
// ✅ BACKEND CALCULATION (DO NOT TRUST FRONTEND)
foreach ($items as &$item) {
$ctn = (float) ($item['ctn'] ?? 0);
$qty = (float) ($item['qty'] ?? 0);
$price = (float) ($item['price'] ?? 0);
$cbm = (float) ($item['cbm'] ?? 0);
$kg = (float) ($item['kg'] ?? 0);
// Calculated fields
$item['ttl_qty'] = $ctn * $qty;
$item['ttl_amount'] = $item['ttl_qty'] * $price;
$item['ttl_cbm'] = $cbm * $ctn;
$item['ttl_kg'] = $ctn * $kg;
}
unset($item); // VERY IMPORTANT
unset($item);
$total_ctn = array_sum(array_column($items, 'ctn'));
$total_qty = array_sum(array_column($items, 'qty'));
$total_ttl_qty = array_sum(array_column($items, 'ttl_qty'));
$total_amount = array_sum(array_column($items, 'ttl_amount'));
$total_cbm = array_sum(array_column($items, 'cbm'));
$total_ttl_cbm = array_sum(array_column($items, 'ttl_cbm'));
$total_kg = array_sum(array_column($items, 'kg'));
$total_ttl_kg = array_sum(array_column($items, 'ttl_kg'));
// 3) totals
$total_ctn = array_sum(array_column($items, 'ctn'));
$total_qty = array_sum(array_column($items, 'qty'));
$total_ttl_qty = array_sum(array_column($items, 'ttl_qty'));
$total_amount = array_sum(array_column($items, 'ttl_amount'));
$total_cbm = array_sum(array_column($items, 'cbm'));
$total_ttl_cbm = array_sum(array_column($items, 'ttl_cbm'));
$total_kg = array_sum(array_column($items, 'kg'));
$total_ttl_kg = array_sum(array_column($items, 'ttl_kg'));
// 4) order id generate
$orderId = $this->generateOrderId();
// 5) order create
$order = Order::create([
'order_id' => $orderId,
'mark_no' => $request->mark_no,
@@ -577,7 +642,6 @@ class AdminOrderController extends Controller
'status' => 'order_placed',
]);
// 6) order items
foreach ($items as $item) {
OrderItem::create([
'order_id' => $order->id,
@@ -596,17 +660,14 @@ class AdminOrderController extends Controller
]);
}
// 7) invoice number
$invoiceNumber = $this->generateInvoiceNumber();
// 8) customer fetch
$markList = MarkList::where('mark_no', $order->mark_no)->first();
$customer = null;
if ($markList && $markList->customer_id) {
$customer = User::where('customer_id', $markList->customer_id)->first();
}
// 9) invoice create
$invoice = Invoice::create([
'order_id' => $order->id,
'customer_id' => $customer->id ?? null,
@@ -631,7 +692,6 @@ class AdminOrderController extends Controller
'pdf_path' => null,
]);
// 10) invoice items
foreach ($order->items as $item) {
InvoiceItem::create([
'invoice_id' => $invoice->id,
@@ -658,114 +718,72 @@ class AdminOrderController extends Controller
* UPDATE ORDER ITEM (existing orders)
* ---------------------------*/
public function updateItem(Request $request, $id)
{
$item = OrderItem::findOrFail($id);
$order = $item->order;
$request->validate([
'description' => 'required|string',
'ctn' => 'nullable|numeric',
'qty' => 'nullable|numeric',
'unit' => 'nullable|string',
'price' => 'nullable|numeric',
'cbm' => 'nullable|numeric',
'kg' => 'nullable|numeric',
'shop_no' => 'nullable|string',
]);
// ✅ BACKEND CALCULATION
$ctn = (float) ($request->ctn ?? 0);
$qty = (float) ($request->qty ?? 0);
$price = (float) ($request->price ?? 0);
$cbm = (float) ($request->cbm ?? 0);
$kg = (float) ($request->kg ?? 0);
$item->update([
'description' => $request->description,
'ctn' => $ctn,
'qty' => $qty,
'ttl_qty' => $ctn * $qty,
'unit' => $request->unit,
'price' => $price,
'ttl_amount' => ($ctn * $qty) * $price,
'cbm' => $cbm,
'ttl_cbm' => $cbm * $ctn,
'kg' => $kg,
'ttl_kg' => $ctn * $kg,
'shop_no' => $request->shop_no,
]);
$this->recalcTotals($order);
$this->updateInvoiceFromOrder($order);
return back()->with('success', 'Item updated successfully');
}
private function updateInvoiceFromOrder(Order $order)
{
$invoice = Invoice::where('order_id', $order->id)->first();
$item = OrderItem::findOrFail($id);
$order = $item->order;
if (!$invoice) {
return;
}
$invoice->final_amount = $order->ttl_amount;
$invoice->gst_percent = 0;
$invoice->gst_amount = 0;
$invoice->final_amount_with_gst = $order->ttl_amount;
$invoice->save();
InvoiceItem::where('invoice_id', $invoice->id)->delete();
foreach ($order->items as $item) {
InvoiceItem::create([
'invoice_id' => $invoice->id,
'description' => $item->description,
'ctn' => $item->ctn,
'qty' => $item->qty,
'ttl_qty' => $item->ttl_qty,
'unit' => $item->unit,
'price' => $item->price,
'ttl_amount' => $item->ttl_amount,
'cbm' => $item->cbm,
'ttl_cbm' => $item->ttl_cbm,
'kg' => $item->kg,
'ttl_kg' => $item->ttl_kg,
'shop_no' => $item->shop_no,
]);
}
}
public function uploadExcelPreview(Request $request)
{
try {
$request->validate([
'excel' => 'required|file|mimes:xlsx,xls'
'description' => 'required|string',
'ctn' => 'nullable|numeric',
'qty' => 'nullable|numeric',
'unit' => 'nullable|string',
'price' => 'nullable|numeric',
'cbm' => 'nullable|numeric',
'kg' => 'nullable|numeric',
'shop_no' => 'nullable|string',
]);
$import = new OrderItemsPreviewImport();
Excel::import($import, $request->file('excel'));
$ctn = (float) ($request->ctn ?? 0);
$qty = (float) ($request->qty ?? 0);
$price = (float) ($request->price ?? 0);
$cbm = (float) ($request->cbm ?? 0);
$kg = (float) ($request->kg ?? 0);
return response()->json([
'success' => true,
'items' => $import->rows
$item->update([
'description' => $request->description,
'ctn' => $ctn,
'qty' => $qty,
'ttl_qty' => $ctn * $qty,
'unit' => $request->unit,
'price' => $price,
'ttl_amount' => ($ctn * $qty) * $price,
'cbm' => $cbm,
'ttl_cbm' => $cbm * $ctn,
'kg' => $kg,
'ttl_kg' => $ctn * $kg,
'shop_no' => $request->shop_no,
]);
} catch (ValidationException $e) {
return response()->json([
'success' => false,
'message' => 'Invalid Excel file format'
], 422);
} catch (\Throwable $e) {
\Log::error($e);
return response()->json([
'success' => false,
'message' => 'Server error'
], 500);
$this->recalcTotals($order);
return back()->with('success', 'Item updated successfully');
}
}
public function uploadExcelPreview(Request $request)
{
try {
$request->validate([
'excel' => 'required|file|mimes:xlsx,xls'
]);
}
$import = new OrderItemsPreviewImport();
Excel::import($import, $request->file('excel'));
return response()->json([
'success' => true,
'items' => $import->rows
]);
} catch (ValidationException $e) {
return response()->json([
'success' => false,
'message' => 'Invalid Excel file format'
], 422);
} catch (\Throwable $e) {
\Log::error($e);
return response()->json([
'success' => false,
'message' => 'Server error'
], 500);
}
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@@ -12,45 +11,98 @@ class AdminReportController extends Controller
/**
* Display the reports page with joined data
*/
public function index(Request $request)
{
// -------------------------------
// FETCH REPORT DATA
// ONLY orders that have BOTH:
// 1. Invoice
// 2. Shipment
// -------------------------------
// public function index(Request $request)
// {
/*********************************************************
* OLD FLOW (Order + Shipment + Invoice)
* फक्त reference साठी ठेवलेला, वापरत नाही.
*********************************************************/
/*
$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(...)
->orderBy('shipments.shipment_date', 'desc')
->get();
*/
/*********************************************************
* NEW FLOW (Container + Invoice + MarkList)
*********************************************************/
// $reports = DB::table('invoices')
// ->join('containers', 'containers.id', '=', 'invoices.containerid')
// ->leftJoin('mark_list', 'mark_list.markno', '=', 'invoices.markno')
// ->select(
// 'invoices.id as invoicepk',
// 'invoices.invoicenumber',
// 'invoices.invoicedate',
// 'invoices.finalamount',
// 'invoices.finalamountwithgst',
// 'invoices.gstpercent',
// 'invoices.gstamount',
// 'invoices.status as invoicestatus',
// 'invoices.markno',
// 'containers.id as containerpk',
// 'containers.containernumber',
// 'containers.containerdate',
// 'containers.containername',
// 'mark_list.companyname',
// 'mark_list.customername'
// )
// ->orderBy('containers.containerdate', 'desc')
// ->get();
// return view('admin.reports', compact('reports'));
// }
public function index(Request $request)
{
$reports = DB::table('invoices')
->join('containers', 'containers.id', '=', 'invoices.container_id')
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
->select(
'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',
// INVOICE
'invoices.id as invoicepk',
'invoices.invoice_number',
'invoices.invoice_date',
'invoices.final_amount',
'invoices.status as invoice_status',
'mark_list.company_name',
'mark_list.customer_name'
'invoices.final_amount_with_gst',
'invoices.gst_percent',
'invoices.gst_amount',
'invoices.status as invoicestatus',
'invoices.mark_no',
// CONTAINER
'containers.id as containerpk',
'containers.container_number',
'containers.container_date',
'containers.container_name',
// RAW FIELDS (for reference/debug if needed)
'invoices.company_name as inv_company_name',
'invoices.customer_name as inv_customer_name',
'mark_list.company_name as ml_company_name',
'mark_list.customer_name as ml_customer_name',
// FINAL FIELDS (automatically pick invoice first, else mark_list)
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name')
)
->orderBy('invoices.invoice_date', 'desc')
->orderBy('invoices.id', 'desc')
->orderBy('shipments.shipment_date', 'desc')
->get();
return view('admin.reports', compact('reports'));
}
}

View File

@@ -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();

View File

@@ -0,0 +1,975 @@
<?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++;
$totalAmount = 0;
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
]);
$totalAmount += $ttlAmount;
}
$invoice->final_amount = $totalAmount;
$invoice->gst_percent = 0;
$invoice->gst_amount = 0;
$invoice->final_amount_with_gst = $totalAmount;
$invoice->save();
}
$msg = "Container '{$container->container_number}' created with {$savedCount} rows and {$invoiceCount} customer invoice(s).";
return redirect()->route('containers.index')->with('success', $msg);
}
public function show(Container $container)
{
$container->load('rows');
$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,
]);
}
}

View File

@@ -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,44 +346,44 @@ public function invoiceDetails($invoice_id)
]);
}
public function confirmOrder($order_id)
{
$user = JWTAuth::parseToken()->authenticate();
// public function confirmOrder($order_id)
// {
// $user = JWTAuth::parseToken()->authenticate();
if (! $user) {
return response()->json([
'success' => false,
'message' => 'Unauthorized'
], 401);
}
// if (! $user) {
// return response()->json([
// 'success' => false,
// 'message' => 'Unauthorized'
// ], 401);
// }
$order = $user->orders()
->where('order_id', $order_id)
->first();
// $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);
// }
// 🚫 Only allow confirm from order_placed
if ($order->status !== 'order_placed') {
return response()->json([
'success' => false,
'message' => 'Order cannot be confirmed'
], 422);
}
// // 🚫 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();
// $order->status = 'order_confirmed';
// $order->save();
return response()->json([
'success' => true,
'message' => 'Order confirmed successfully'
]);
}
// return response()->json([
// 'success' => true,
// 'message' => 'Order confirmed successfully'
// ]);
// }

View File

@@ -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
View 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);
}
}

View 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);
}
}

View File

@@ -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
);
}
}

View 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');
}
}

View 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');
}
}

View File

@@ -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;
}
}

View File

@@ -58,10 +58,10 @@ class Order extends Model
return $this->belongsToMany(\App\Models\Shipment::class, 'shipment_items', 'order_id', 'shipment_id');
}
public function invoice()
{
return $this->hasOne(\App\Models\Invoice::class, 'order_id', 'id');
}
// public function invoice()
// {
// return $this->hasOne(\App\Models\Invoice::class, 'order_id', 'id');
// }
const STATUS_LABELS = [

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -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');
}
};

View File

@@ -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');
}
};

View File

@@ -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');
}
};

View File

@@ -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');
});
}
};

View File

@@ -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');
});
}
};

View File

@@ -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');
}
};

View File

@@ -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');
}
};

View File

@@ -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']);
});
}
};

View File

@@ -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');
}
});
}
};

View File

@@ -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();
});
}
};

View File

@@ -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']);
});
}
};

View File

@@ -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');
}
});
}
}

View File

@@ -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'
");
}
};

View File

@@ -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');
});
}
};

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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>

View File

@@ -6,6 +6,7 @@
<meta name="csrf-token" content="{{ csrf_token() }}">
<style>
/* ---------- Base ---------- */
:root{
--primary-1:#1a2951;
--primary-2:#243a72;
@@ -37,7 +38,7 @@ body {
/* header */
.account-header {
margin-bottom: 18px;
background: linear-gradient(90deg,var(--primary-1),var(--primary-2));
background:linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 22px 26px;
border-radius: var(--rounded);
box-shadow: 0 6px 18px rgba(34,50,90,0.12);
@@ -62,7 +63,7 @@ body {
.btn {
display:inline-flex; align-items:center; justify-content:center; gap:8px;
background: linear-gradient(90deg,var(--primary-1),var(--primary-2));
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color:#fff; border:none; padding:10px 16px; border-radius:10px; font-weight:600;
cursor:pointer; transition: transform .15s ease, box-shadow .15s;
}
@@ -334,7 +335,7 @@ tr:hover td{ background:#fbfdff; }
display: flex;
align-items: center;
gap: 8px;
/* margin-right:-550px; */
margin-right:-550px;
}
@@ -342,7 +343,7 @@ tr:hover td{ background:#fbfdff; }
display: flex;
align-items: center;
gap: 8px;
margin-right:-550px;
margin-right:8=500px;
}
@@ -458,7 +459,7 @@ tr:hover td{ background:#fbfdff; }
filter: brightness(0) saturate(100%) invert(84%) sepia(8%) saturate(165%) hue-rotate(179deg) brightness(89%) contrast(86%);
}
/* ---------- Entry Details Modal (existing) ---------- */
/* ---------- Entry Details Modal (Enhanced) ---------- */
.modal-fade1 {
position:fixed;
inset:0;
@@ -471,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;
@@ -489,7 +493,7 @@ tr:hover td{ background:#fbfdff; }
.entry-summary-cards {
display:flex;
gap:16px;
margin-bottom:20px;
margin-top:25px;
flex-wrap:wrap;
}
.entry-summary-card {
@@ -504,6 +508,184 @@ tr:hover td{ background:#fbfdff; }
.entry-summary-label{ font-size:12px; color:var(--muted); }
.entry-summary-value{ font-size:18px; font-weight:700; color:#253047; margin-top:6px; }
/* Enhanced dropdown */
.installment-status-dropdown {
cursor: pointer;
appearance: none;
width: 100%;
padding: 10px 40px 10px 12px;
border-radius: 8px;
border: 1.5px solid #e3eaf6;
background: white;
font-size: 13px;
font-weight: 500;
transition: all 0.2s ease;
background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%236b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>');
background-repeat: no-repeat;
background-position: right 12px center;
min-width: 140px;
}
.installment-status-dropdown:hover {
border-color: #c2d1f0;
background-color: #f8fafc;
}
.installment-status-dropdown:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(39, 109, 234, 0.1);
}
/* Status-specific dropdown options */
.installment-status-dropdown option {
padding: 12px !important;
font-size: 13px;
font-weight: 500;
}
.installment-status-dropdown option[value="Pending"] {
color: #f59e0b;
}
.installment-status-dropdown option[value="Loading"] {
color: #3b82f6;
}
.installment-status-dropdown option[value="Packed"] {
color: #8b5cf6;
}
.installment-status-dropdown option[value="Dispatched"] {
color: #10b981;
}
.installment-status-dropdown option[value="Delivered"] {
color: #0c6b2e;
}
/* ---------- Entry Orders Modal (Enhanced) ---------- */
.entry-orders-modal .modal-box1 {
padding: 0 !important;
overflow: hidden;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(18, 30, 60, 0.25);
}
.entry-orders-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 24px 30px;
color: white;
border-radius: 16px 16px 0 0;
}
.entry-orders-header h2 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 700;
color: white;
}
.entry-orders-header .subtitle {
font-size: 14px;
opacity: 0.9;
display: flex;
align-items: center;
gap: 8px;
}
.entry-orders-content {
padding: 30px;
max-height: 70vh;
overflow-y: auto;
}
/* Orders table in modal */
.orders-table-container {
max-height: 400px;
overflow-y: auto;
border: 1px solid #eef3fb;
border-radius: 12px;
margin: 20px 0;
}
.orders-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.orders-table th {
background: linear-gradient(90deg, #f8fbff, #f5f9ff);
padding: 14px 16px;
text-align: left;
font-weight: 700;
color: var(--primary-1);
border-bottom: 2px solid #eef3fb;
position: sticky;
top: 0;
}
.orders-table td {
padding: 14px 16px;
border-bottom: 1px solid #f1f6ff;
}
.orders-table tr:hover {
background: #fbfdff;
}
.orders-table tr:last-child td {
border-bottom: none;
}
/* Order ID with badge style */
.order-id-badge {
display: inline-block;
background: linear-gradient(90deg, #f0f7ff, #e6f0ff);
color: var(--primary-1);
padding: 4px 10px;
border-radius: 6px;
font-weight: 600;
font-size: 12px;
border: 1px solid #dbe4f5;
}
/* Summary row */
.orders-summary-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: linear-gradient(90deg, #e6ebf5 0%, #f9fbff 100%);
border-radius: 10px;
margin-top: 20px;
border: 1px solid #eef3fb;
}
.orders-summary-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.orders-summary-value {
font-size: 20px;
font-weight: 800;
color: var(--primary-1);
}
.orders-summary-label {
font-size: 12px;
color: var(--muted);
margin-top: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* installment modal */
#installmentModal .modal-box1 { max-width:720px; min-width:380px; }
@@ -828,7 +1010,10 @@ tr:hover td{ background:#fbfdff; }
flex-wrap: wrap;
margin-bottom: 16px;
padding: 12px 14px;
background: linear-gradient(90deg, #f9fbff, #f7faff);
background:linear-gradient(90deg, #e6ebf5 0%, #f9fbff 100%);
border-radius: 8px;
border: 1px solid #eef3fb;
}
@@ -1110,6 +1295,40 @@ tr:hover td{ background:#fbfdff; }
.pagination-container {
margin-right: 0 !important;
}
/* Responsive modals */
.entry-details-modal .modal-box1,
.entry-orders-modal .modal-box1 {
margin: 10px;
border-radius: 12px;
}
.entry-details-header,
.entry-orders-header {
padding: 18px 20px;
}
.entry-details-content,
.entry-orders-content {
padding: 20px;
}
.entry-summary-cards {
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
}
.entry-summary-value {
font-size: 20px;
}
.orders-summary-row {
flex-direction: column;
gap: 15px;
text-align: center;
}
}
/* Zoom out specific fix */
@@ -1146,6 +1365,10 @@ tr:hover td{ background:#fbfdff; }
.pagination-info {
font-size: 12px;
}
.entry-summary-cards {
grid-template-columns: 1fr;
}
}
/* Prevent horizontal scroll on very small screens */
@@ -1178,6 +1401,7 @@ html, body {
</style>
<div class="account-container">
@@ -1323,7 +1547,7 @@ html, body {
<!-- CREATE ORDER POPUP MODAL -->
<div class="create-order-modal" id="createOrderModal">
<div class="modal-box">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:16px;">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:16px; background:linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding:20px; border-radius:12px; color:white; margin-top:-20px; margin-left:-20px; margin-right:-15px; margin-top:-15px;">
<div style="font-size:20px; font-weight:800;">Create New Installment</div>
<button class="btn ghost" id="closeCreateModal" title="Close create form"></button>
</div>
@@ -1415,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>
@@ -2331,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>
@@ -2408,7 +2681,6 @@ function renderPaymentTable(list){
function cycleToggle(btn) {
// वर्तमान position घेऊन पुढचा स्टेट कॅल्क्युलेट करा
let pos = parseInt(btn.dataset.pos || '0', 10); // 0 = unpaid, 1 = pending, 2 = paid
@@ -2784,76 +3056,98 @@ async function submitEditEntry(e) {
}
function openEntryOrdersModal(entryNo) {
document.getElementById('entryOrdersEntryNo-span').textContent = `(${entryNo})`;
function openEntryOrdersModal(entryNo) {
// Set entry number in header
document.getElementById('entryOrdersEntryNo-span').textContent = `(${entryNo})`;
const tbody = document.getElementById('entryOrdersTableBody');
// 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>
<td colspan="7" class="empty-state">Loading orders...</td>
</tr>
`;
// API call: /admin/account/entry-orders/{entryno}
jsonFetch(`/admin/account/entry-orders/${encodeURIComponent(entryNo)}`, {
method: 'GET'
})
.then(res => {
if (!res.success) {
tbody.innerHTML = `
<tr>
<td colspan="7" class="empty-state">Loading orders...</td>
</tr>
<tr>
<td colspan="7" class="empty-state">Failed to load orders</td>
</tr>
`;
return;
}
jsonFetch(`/admin/account/entry-orders/${encodeURIComponent(entryNo)}`, {
method: 'GET'
})
.then(res => {
if (!res.success) {
tbody.innerHTML = `
<tr>
<td colspan="7" class="empty-state">Failed to load orders</td>
</tr>
`;
return;
}
const orders = res.orders || [];
if (!orders.length) {
tbody.innerHTML = `
<tr>
<td colspan="7" class="empty-state">No orders associated with this entry</td>
</tr>
`;
return;
}
const orders = res.orders || [];
if (!orders.length) {
tbody.innerHTML = `
<tr>
<td colspan="7" class="empty-state">No orders associated with this entry</td>
</tr>
`;
return;
}
tbody.innerHTML = '';
let totalQuantity = 0;
let totalAmount = 0;
tbody.innerHTML = '';
orders.forEach(order => {
const tr = document.createElement('tr');
orders.forEach(order => {
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 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>${escapeHtml(order.order_id)}</td>
<td>${escapeHtml(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>
`;
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);
});
tbody.appendChild(tr);
});
})
.catch(() => {
tbody.innerHTML = `
<tr>
<td colspan="7" class="empty-state">Error loading orders</td>
</tr>
`;
});
// Update summary
document.getElementById('ordersTotalCount').textContent = orders.length;
document.getElementById('ordersTotalQuantity').textContent = totalQuantity.toLocaleString();
document.getElementById('ordersTotalAmount').textContent = formatCurrency(totalAmount);
document.getElementById('entryOrdersModal').classList.add('modal-open');
}
})
.catch(() => {
tbody.innerHTML = `
<tr>
<td colspan="7" class="empty-state">Error loading orders</td>
</tr>
`;
});
document.getElementById('entryOrdersModal').classList.add('modal-open');
}
function closeEntryOrdersModal() {
document.getElementById('entryOrdersModal').classList.remove('modal-open');
@@ -2989,7 +3283,7 @@ function handleSearch(){
updatePaginationControls();
}
/* ---------- Entry details & installments ---------- */
/* ---------- Entry details & installments (Enhanced) ---------- */
async function openEntryDetailsModal(entryNo) {
try {
const res = await jsonFetch('/admin/account/entry/' + encodeURIComponent(entryNo));
@@ -2999,58 +3293,109 @@ async function openEntryDetailsModal(entryNo) {
const entry = res.entry;
currentEntry = entry;
// Set header info
document.getElementById('entryDetailsId').textContent = entry.entry_no;
document.getElementById('originalAmount').textContent = formatCurrency(entry.amount);
const totalProcessed = Number(entry.amount) - Number(entry.pending_amount);
// Calculate values
const originalAmount = Number(entry.amount || 0);
const pendingAmount = Number(entry.pending_amount || 0);
const totalProcessed = originalAmount - pendingAmount;
// Update summary cards
document.getElementById('originalAmount').textContent = formatCurrency(originalAmount);
document.getElementById('totalProcessed').textContent = formatCurrency(totalProcessed);
document.getElementById('pendingBalance').textContent = formatCurrency(pendingAmount);
document.getElementById('totalInstallments').textContent = entry.installments?.length || 0;
document.getElementById('pendingBalance').textContent = formatCurrency(entry.pending_amount);
document.getElementById('totalInstallments').textContent = entry.installments.length;
// Render installments table
const tbody = document.getElementById('installmentsTableBody');
tbody.innerHTML = '';
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) {

View 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

View 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

View 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>

View File

@@ -0,0 +1,977 @@
@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;
}
/* DOWNLOAD BUTTONS - NEW STYLES */
.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 0 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;
}
}
.cm-table-scroll-outer {
margin: 10px 14px 30px 14px; /* फक्त हे बदल करा */
}
</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 autocalculate 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>
<!-- बाकीचा सगळा code same आहे - काही बदल नाही -->
<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
<!-- Toast notification missing होती, add केली -->
<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);
}
function parseNumber(str) {
if (!str) return 0;
const cleaned = String(str).replace(/,/g, '').trim();
const val = parseFloat(cleaned);
return isNaN(val) ? 0 : val;
}
function formatNumber(val, decimals) {
if (isNaN(val)) val = 0;
return val.toFixed(decimals);
}
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;
}
});
if (ttlQtyInput && ctnInput && qtyInput) {
const newTtlQty = ctn * qty;
ttlQtyInput.value = formatNumber(newTtlQty, 0);
ttlQty = newTtlQty;
}
if (ttlCbmInput && cbmInput && ctnInput) {
const newTtlCbm = cbm * ctn;
ttlCbmInput.value = formatNumber(newTtlCbm, 3);
ttlCbm = newTtlCbm;
}
if (ttlKgInput && kgInput && ctnInput) {
const newTtlKg = kg * ctn;
ttlKgInput.value = formatNumber(newTtlKg, 2);
ttlKg = newTtlKg;
}
if (amountInput && priceInput && ttlQtyInput) {
const newAmount = price * ttlQty;
amountInput.value = formatNumber(newAmount, 2);
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

View File

@@ -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,8 @@
<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">Total Payable</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 +672,106 @@
<tbody id="customersTableBody">
@forelse($customers as $c)
@php
// Invoice total (with GST)
$totalPayable = $c->invoices->sum('final_amount_with_gst');
// Orders = invoice count
$ordersCount = $c->invoices->count();
// Order Total = items total from all invoices (final_amount)
$orderTotal = $c->invoices->sum('final_amount');
// Total payable = grand total with GST + groups
$totalPayable = $c->invoices->sum('grand_total_with_charges');
// Total paid via installments
$totalPaid = $c->invoices
->flatMap(fn($inv) => $inv->installments)
->sum('amount');
$totalPaid = $c->invoiceInstallments->sum('amount');
// Remaining amount
$remainingAmount = max($totalPayable - $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($totalPayable, 2) }}
</span>
@endif
</td>
</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>
<td>
@if($c->status === 'active')
<span class="status-badge active-status">Active</span>
@else
<span class="status-badge inactive-status">Inactive</span>
@endif
</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>
<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>
<!-- 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>
<!-- 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="9" 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 +782,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 +801,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 +811,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 +822,6 @@
});
});
// Pagination button handlers
document.getElementById('prevPageBtn').addEventListener('click', function() {
@if(!$customers->onFirstPage())
window.location.href = '{{ $customers->previousPageUrl() }}';
@@ -1015,4 +836,4 @@
});
</script>
@endsection
@endsection

View File

@@ -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

View File

@@ -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 %</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="gst-cell">
{{ $invoice->gst_percent }}%
</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">
@@ -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,6 +1689,10 @@ 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.final_amount_with_gst).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</td>
@@ -1573,6 +1701,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>
</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,6 +1744,13 @@ 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>

File diff suppressed because it is too large Load Diff

View File

@@ -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

View 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>

View 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>

View 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

View File

@@ -5,242 +5,421 @@
@section('content')
<div class="container-fluid px-0 bg-transparent">
<style>
body {
background: linear-gradient(135deg, #f2f6ff, #fefefe);
font-family: "Poppins", sans-serif;
}
/* PROFILE HEADER */
/* ── HEADER ── */
.profile-header {
display: flex;
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
gap: 1rem;
padding: 36px 32px;
background: linear-gradient(120deg, #4e73df 68%, #1cc88a 100%);
border-radius: 20px;
gap: 1.5rem;
padding: 32px 36px;
background: linear-gradient(120deg, #4e73df 60%, #1cc88a 100%);
border-radius: 22px;
color: white;
box-shadow: 0 8px 28px rgba(24,40,90,0.09);
margin-bottom: 34px;
box-shadow: 0 10px 36px rgba(78,115,223,0.22);
margin-bottom: 28px;
position: relative;
min-height: 140px;
overflow: hidden;
}
.profile-header::before {
content:"";position:absolute;top:-60px;right:-60px;
width:220px;height:220px;
background:rgba(255,255,255,0.08);border-radius:50%;
}
.profile-header::after {
content: "";
position: absolute;
top: -58px;
right: -85px;
width: 210px;
height: 210px;
background: rgba(255,255,255,0.14);
border-radius: 50%;
}
.avatar {
width: 98px;
height: 98px;
border-radius: 50%;
object-fit: cover;
border: 4px solid rgba(255,255,255,0.7);
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
background: #fff;
z-index: 1;
}
.main-info {
display: flex;
flex-direction: column;
gap: 10px;
z-index: 1;
flex: 1;
}
.main-row {
display: flex;
flex-wrap: wrap;
gap: 25px;
}
.field-group {
min-width: 210px;
}
.label {
font-size: 0.9rem;
color: #dbe7f4;
font-weight: 500;
}
.value {
font-size: 1.1rem;
font-weight: 600;
color: #fff;
background: rgba(255,255,255,0.13);
border-radius: 8px;
padding: 6px 12px;
border: 1.5px solid rgba(255,255,255,0.18);
transition: background 0.13s;
}
.value:hover { background: rgba(255,255,255,0.22); }
/* STATS + CARDS COMMON */
.profile-stats-row, .profile-grid-row {
display: flex;
flex-wrap: wrap;
gap: 18px;
margin-bottom: 20px;
}
.stat-card, .profile-card {
background: #fff;
border-radius: 14px;
box-shadow: 0 3px 18px rgba(60,80,120,0.08);
padding: 20px;
transition: all 0.3s ease;
position: relative;
flex: 1 1 200px;
}
.stat-card:hover, .profile-card:hover {
transform: translateY(-4px);
box-shadow: 0 6px 25px rgba(50,70,120,0.18);
content:"";position:absolute;bottom:-80px;right:180px;
width:180px;height:180px;
background:rgba(255,255,255,0.05);border-radius:50%;
}
/* STATS */
.stat-card::before {
content: "";
position: absolute;
top: -20px;
right: -20px;
width: 90px;
height: 90px;
background: linear-gradient(135deg, #c4d7ff, #e9f0ff);
border-radius: 50%;
opacity: 0.25;
.avatar-initials {
width:100px;height:100px;border-radius:50%;
border:4px solid rgba(255,255,255,0.75);
box-shadow:0 6px 24px rgba(0,0,0,0.18);
background:#fff;z-index:1;flex-shrink:0;
display:flex;align-items:center;justify-content:center;
font-size:2.6rem;font-weight:800;color:#4e73df;
letter-spacing:-1px;
}
.main-info { display:flex;flex-direction:column;gap:14px;z-index:1;flex:1; }
.main-row { display:flex;flex-wrap:wrap;gap:18px;align-items:flex-start; }
.field-group { min-width:160px; }
.ph-label {
font-size:0.72rem;color:rgba(255,255,255,0.65);
font-weight:600;text-transform:uppercase;letter-spacing:0.07em;margin-bottom:3px;
}
.ph-value {
font-size:0.95rem;font-weight:600;color:#fff;
background:rgba(255,255,255,0.12);border-radius:9px;
padding:6px 13px;border:1.5px solid rgba(255,255,255,0.2);
backdrop-filter:blur(4px);
}
.type-badge {
display:inline-block;padding:4px 14px;border-radius:50px;
font-size:0.72rem;font-weight:700;letter-spacing:0.07em;text-transform:uppercase;
}
.type-admin { background:rgba(255,210,60,0.22);color:#ffe066;border:1px solid rgba(255,220,80,0.4); }
.type-staff { background:rgba(100,255,180,0.18);color:#a0ffda;border:1px solid rgba(100,255,180,0.35); }
.status-dot {
display:inline-block;width:9px;height:9px;
border-radius:50%;margin-right:5px;
}
.status-active { background:#22c55e;box-shadow:0 0 5px #22c55e; }
.status-inactive { background:#ef4444;box-shadow:0 0 5px #ef4444; }
/* ── STATS ── */
.profile-stats-row { display:flex;flex-wrap:wrap;gap:16px;margin-bottom:24px; }
.stat-card {
background:#fff;border-radius:16px;
box-shadow:0 3px 18px rgba(60,80,120,0.07);
padding:20px 22px;
transition:all 0.3s ease;
position:relative;
flex:1 1 160px;
overflow:hidden;
}
.stat-card::after {
content:"";position:absolute;
top:-25px;right:-25px;
width:80px;height:80px;
border-radius:50%;
opacity:0.35;
}
/* Per-card accent colors */
.stat-card.sc-blue { border-top:3px solid #4e73df; }
.stat-card.sc-blue::after { background:linear-gradient(135deg,#c4d7ff,#e9f0ff); }
.stat-card.sc-green { border-top:3px solid #1cc88a; }
.stat-card.sc-green::after { background:linear-gradient(135deg,#bbf7d0,#dcfce7); }
.stat-card.sc-yellow { border-top:3px solid #f6c23e; }
.stat-card.sc-yellow::after { background:linear-gradient(135deg,#fef9c3,#fef08a); }
.stat-card.sc-red { border-top:3px solid #e74a3b; }
.stat-card.sc-red::after { background:linear-gradient(135deg,#fee2e2,#fecaca); }
.stat-card.sc-purple { border-top:3px solid #8b5cf6; }
.stat-card.sc-purple::after { background:linear-gradient(135deg,#ede9fe,#ddd6fe); }
.stat-card.sc-teal { border-top:3px solid #0ea5e9; }
.stat-card.sc-teal::after { background:linear-gradient(135deg,#e0f2fe,#bae6fd); }
.stat-card:hover { transform:translateY(-5px);box-shadow:0 10px 30px rgba(78,115,223,0.15); }
.stat-icon-wrap {
width:42px;height:42px;border-radius:11px;
display:flex;align-items:center;justify-content:center;
font-size:1.25rem;margin-bottom:10px;
}
.sc-blue .stat-icon-wrap { background:#eff6ff; }
.sc-green .stat-icon-wrap { background:#f0fdf4; }
.sc-yellow .stat-icon-wrap { background:#fefce8; }
.sc-red .stat-icon-wrap { background:#fef2f2; }
.sc-purple .stat-icon-wrap { background:#faf5ff; }
.sc-teal .stat-icon-wrap { background:#f0f9ff; }
.stat-label {
font-size: 0.9rem;
color: #8b96ad;
font-weight: 500;
font-size:0.78rem;color:#8b96ad;font-weight:600;
text-transform:uppercase;letter-spacing:0.06em;
margin-bottom:4px;display:block;
}
.stat-value {
font-size: 1.6rem;
font-weight: 700;
display: flex;
align-items: center;
color: #1b2330;
font-size:1.7rem;font-weight:800;color:#1b2330;
line-height:1;margin-bottom:6px;
}
.stat-footer { font-size: 0.9rem; }
.positive { color: #23a25d; }
.negative { color: #e54141; }
.neutral { color: #9a9ea8; }
.stat-footer { font-size:0.78rem;color:#94a3b8; }
.positive { color:#16a34a; }
.negative { color:#dc2626; }
/* PROFILE CARDS */
/* ── PROFESSIONAL INFO ── */
.pro-section { margin-bottom:26px; }
.pro-section-title {
font-size:0.88rem;font-weight:700;color:#1b2330;
border-left:4px solid #4e73df;padding-left:9px;margin-bottom:14px;
display:flex;align-items:center;gap:7px;
}
.pro-grid { display:flex;flex-wrap:wrap;gap:14px; }
.pro-box {
background:#fff;border-radius:14px;
padding:16px 20px;
box-shadow:0 2px 14px rgba(60,80,120,0.07);
border:1px solid #e8edf5;
transition:all 0.25s cubic-bezier(.4,0,.2,1);
flex:1 1 160px;min-width:0;
position:relative;overflow:hidden;
}
.pro-box::before {
content:"";
position:absolute;top:0;left:0;right:0;height:3px;
background:linear-gradient(90deg,#4e73df,#1cc88a);
opacity:0;transition:opacity 0.25s;
}
.pro-box:hover { transform:translateY(-4px);box-shadow:0 8px 28px rgba(78,115,223,0.15);border-color:#c7d4f8; }
.pro-box:hover::before { opacity:1; }
.box-icon { font-size:1.3rem;margin-bottom:7px;display:inline-block; }
.box-label { font-size:0.68rem;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:4px; }
.box-value { font-size:0.95rem;font-weight:700;color:#1b2330;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }
/* ── BOTTOM CARDS ── */
.profile-grid-row { display:flex;flex-wrap:wrap;gap:18px;margin-bottom:10px; }
.profile-card {
background:#fff;border-radius:16px;
box-shadow:0 3px 18px rgba(60,80,120,0.07);
padding:22px;transition:all 0.3s ease;flex:1 1 220px;
}
.profile-card:hover { transform:translateY(-4px);box-shadow:0 8px 28px rgba(78,115,223,0.14); }
.profile-card h6 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 12px;
color: #1b2330;
border-left: 4px solid #3767f4;
padding-left: 8px;
font-size:1rem;font-weight:700;margin-bottom:14px;color:#1b2330;
border-left:4px solid #4e73df;padding-left:9px;
}
.activity-list, .quick-action-list {
list-style: none;
padding: 0;
margin: 0;
.activity-list,.quick-action-list { list-style:none;padding:0;margin:0; }
.activity-list li,.quick-action-list li {
padding:9px 6px;font-size:0.92rem;border-bottom:1px solid #f2f4f8;
display:flex;align-items:center;gap:8px;transition:0.2s ease;border-radius:6px;
}
.activity-list li, .quick-action-list li {
padding: 9px 2px;
font-size: 1rem;
border-bottom: 1px solid #f2f4f8;
display: flex;
align-items: center;
gap: 8px;
transition: 0.2s ease;
}
.activity-list li:hover { background: #f7faff; border-radius: 6px; }
.activity-list li:hover { background:#f4f7ff;padding-left:10px; }
.quick-action-list li a {
text-decoration: none;
color: #2563eb;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
transition: 0.2s ease;
text-decoration:none;color:#2563eb;font-weight:600;
display:flex;align-items:center;gap:8px;transition:0.2s ease;width:100%;
}
.quick-action-list li a:hover { color: #1e40af; transform: translateX(4px); }
.quick-action-list li a:hover { color:#1e40af;transform:translateX(5px); }
@media (max-width: 1000px) {
.profile-header, .profile-stats-row, .profile-grid-row {
flex-direction: column;
text-align: center;
}
.avatar { margin: 0 auto 15px; }
.main-row { justify-content: center; }
@media (max-width:900px) {
.profile-header,.profile-stats-row,.profile-grid-row { flex-direction:column; }
.avatar-initials { margin:0 auto; }
.main-row { justify-content:center; }
.pro-grid { flex-direction:column; }
.pro-box { flex:unset;width:100%; }
}
</style>
<!-- PROFILE HEADER -->
@php
$user = $user ?? Auth::guard('admin')->user();
$isAdmin = $user->type === 'admin';
$initials = strtoupper(substr($user->name ?? 'U', 0, 1));
$joiningFormatted = $user->joining_date
? \Carbon\Carbon::parse($user->joining_date)->format('d M Y')
: '—';
$isActive = ($user->status ?? 'active') === 'active';
// stats — controller मधून येतात, fallback 0
$s = $stats ?? [];
$totalContainers = $s['total_containers'] ?? 0;
$totalInvoices = $s['total_invoices'] ?? 0;
$paidInvoices = $s['paid_invoices'] ?? 0;
$pendingInvoices = $s['pending_invoices'] ?? 0;
$totalCustomers = $s['total_customers'] ?? 0;
$totalMarklist = $s['total_marklist'] ?? 0;
$activeMarklist = $s['active_marklist'] ?? 0;
@endphp
{{-- ════════ PROFILE HEADER ════════ --}}
<div class="profile-header">
<img src="https://i.pravatar.cc/100" class="avatar" alt="Avatar" />
<div class="avatar-initials">{{ $initials }}</div>
<div class="main-info">
{{-- Row 1 --}}
<div class="main-row">
<div class="field-group">
<div class="label">Name</div>
<div class="value">John Doe</div>
<div class="ph-label">Full Name</div>
<div class="ph-value" style="font-size:1.05rem;">{{ $user->name ?? '—' }}</div>
</div>
<div class="field-group">
<div class="label">Email</div>
<div class="value">john@example.com</div>
<div class="ph-label">Account Type</div>
<div class="ph-value" style="padding:4px 10px;">
<span class="type-badge {{ $isAdmin ? 'type-admin' : 'type-staff' }}">
{{ $isAdmin ? '⭐ Admin' : '👤 Staff' }}
</span>
</div>
</div>
<div class="field-group">
<div class="label">Mobile No</div>
<div class="value">+91 12345 54321</div>
<div class="ph-label">Status</div>
<div class="ph-value">
<span class="status-dot {{ $isActive ? 'status-active' : 'status-inactive' }}"></span>
{{ ucfirst($user->status ?? 'active') }}
</div>
</div>
@if($user->employee_id)
<div class="field-group">
<div class="ph-label">Employee ID</div>
<div class="ph-value" style="font-family:monospace;letter-spacing:0.05em;">{{ $user->employee_id }}</div>
</div>
@endif
</div>
{{-- Row 2 --}}
<div class="main-row">
<div class="field-group">
<div class="label">Company Name</div>
<div class="value">XYZ Infotech</div>
<div class="ph-label">Email</div>
<div class="ph-value" style="font-size:0.88rem;">{{ $user->email ?? '—' }}</div>
</div>
<div class="field-group" style="min-width:260px;">
<div class="label">Company Address</div>
<div class="value">XYZ Infotech, East Andheri, Mumbai 400 001</div>
<div class="field-group">
<div class="ph-label">Phone</div>
<div class="ph-value">{{ $user->phone ?? '—' }}</div>
</div>
@if($user->department)
<div class="field-group">
<div class="ph-label">Department</div>
<div class="ph-value">{{ $user->department }}</div>
</div>
@endif
@if($user->designation)
<div class="field-group">
<div class="ph-label">Designation</div>
<div class="ph-value">{{ $user->designation }}</div>
</div>
@endif
@if($user->address)
<div class="field-group" style="min-width:220px;">
<div class="ph-label">Address</div>
<div class="ph-value" style="font-size:0.88rem;">{{ $user->address }}</div>
</div>
@endif
</div>
</div>
</div>
<!-- STATS ROW -->
{{-- ════════ PROFESSIONAL INFO ════════ --}}
<div class="pro-section">
<div class="pro-section-title">🏢 Professional Information</div>
<div class="pro-grid">
@if($user->employee_id)
<div class="pro-box">
<div class="box-icon">🪪</div>
<div class="box-label">Employee ID</div>
<div class="box-value" style="font-family:monospace;">{{ $user->employee_id }}</div>
</div>
@endif
@if($user->username)
<div class="pro-box">
<div class="box-icon">👤</div>
<div class="box-label">Username</div>
<div class="box-value">{{ $user->username }}</div>
</div>
@endif
@if($user->role)
<div class="pro-box">
<div class="box-icon">🎯</div>
<div class="box-label">Role</div>
<div class="box-value">{{ $user->role }}</div>
</div>
@endif
@if($user->department)
<div class="pro-box">
<div class="box-icon">🏬</div>
<div class="box-label">Department</div>
<div class="box-value">{{ $user->department }}</div>
</div>
@endif
@if($user->designation)
<div class="pro-box">
<div class="box-icon">📋</div>
<div class="box-label">Designation</div>
<div class="box-value">{{ $user->designation }}</div>
</div>
@endif
<div class="pro-box">
<div class="box-icon">📅</div>
<div class="box-label">Joining Date</div>
<div class="box-value">{{ $joiningFormatted }}</div>
</div>
@if($user->emergency_phone)
<div class="pro-box">
<div class="box-icon">🆘</div>
<div class="box-label">Emergency Phone</div>
<div class="box-value">{{ $user->emergency_phone }}</div>
</div>
@endif
@if($user->additional_info)
<div class="pro-box" style="flex:2 1 320px;">
<div class="box-icon">📝</div>
<div class="box-label">Additional Info</div>
<div class="box-value" style="font-size:0.88rem;font-weight:500;color:#475569;white-space:normal;">{{ $user->additional_info }}</div>
</div>
@endif
</div>
</div>
{{-- ════════ REAL STATS ════════ --}}
<div class="profile-stats-row">
<div class="stat-card">
<span class="stat-label">Total Shipment</span>
<span class="stat-value">📦 1,250</span>
<div class="stat-footer positive">+12% vs last month</div>
<div class="stat-card sc-blue">
<div class="stat-icon-wrap">📦</div>
<span class="stat-label">Total Containers</span>
<div class="stat-value">{{ number_format($totalContainers) }}</div>
<div class="stat-footer">All containers in system</div>
</div>
<div class="stat-card">
<span class="stat-label">In Transit</span>
<span class="stat-value" style="color:#2563eb;">🚚 320</span>
<div class="stat-footer neutral">65% of capacity</div>
<div class="stat-card sc-green">
<div class="stat-icon-wrap">🧾</div>
<span class="stat-label">Total Invoices</span>
<div class="stat-value">{{ number_format($totalInvoices) }}</div>
<div class="stat-footer positive">
{{ $paidInvoices }} paid
</div>
</div>
<div class="stat-card">
<span class="stat-label">At Customs</span>
<span class="stat-value" style="color:#f59e0b;">🛄 85</span>
<div class="stat-footer neutral">Avg 2.3 days processing</div>
<div class="stat-card sc-yellow">
<div class="stat-icon-wrap"></div>
<span class="stat-label">Pending Invoices</span>
<div class="stat-value" style="color:#d97706;">{{ number_format($pendingInvoices) }}</div>
<div class="stat-footer">Awaiting payment</div>
</div>
<div class="stat-card">
<span class="stat-label">Delayed</span>
<span class="stat-value" style="color:#e11d48;"> 55</span>
<div class="stat-footer negative">-8% vs last month</div>
<div class="stat-card sc-teal">
<div class="stat-icon-wrap"></div>
<span class="stat-label">Paid Invoices</span>
<div class="stat-value" style="color:#0ea5e9;">{{ number_format($paidInvoices) }}</div>
<div class="stat-footer positive">
@if($totalInvoices > 0)
{{ round(($paidInvoices / $totalInvoices) * 100, 1) }}% success rate
@else
No invoices yet
@endif
</div>
</div>
<div class="stat-card">
<span class="stat-label">Delivered</span>
<span class="stat-value" style="color:#22c55e;"> 780</span>
<div class="stat-footer positive">98.2% success rate</div>
<div class="stat-card sc-purple">
<div class="stat-icon-wrap">👥</div>
<span class="stat-label">Total Customers</span>
<div class="stat-value" style="color:#8b5cf6;">{{ number_format($totalCustomers) }}</div>
<div class="stat-footer">Registered customers</div>
</div>
<div class="stat-card sc-red">
<div class="stat-icon-wrap">🏷️</div>
<span class="stat-label">Mark List</span>
<div class="stat-value" style="color:#e74a3b;">{{ number_format($totalMarklist) }}</div>
<div class="stat-footer positive">
{{ $activeMarklist }} active
</div>
</div>
</div>
<!-- BOTTOM CARDS -->
{{-- ════════ BOTTOM CARDS ════════ --}}
<div class="profile-grid-row">
<div class="profile-card">
<h6>Recent Activity</h6>
<ul class="activity-list">
<li>📦 Shipment #SH-2024-001 delivered <span style="color:#a1a5ad;font-size:0.9em;margin-left:8px;">5 min ago</span></li>
<li>📝 New shipment #SH-2024-002 <span style="color:#a1a5ad;font-size:0.9em;margin-left:8px;">13 min ago</span></li>
<li> Delay for #SH-2024-003 <span style="color:#a1a5ad;font-size:0.9em;margin-left:8px;">1 hr ago</span></li>
<li>📦 Shipment #SH-2024-001 delivered <span style="color:#a1a5ad;font-size:0.85em;margin-left:auto;">5 min ago</span></li>
<li>📝 New shipment #SH-2024-002 created <span style="color:#a1a5ad;font-size:0.85em;margin-left:auto;">13 min ago</span></li>
<li> Delay reported for #SH-2024-003 <span style="color:#a1a5ad;font-size:0.85em;margin-left:auto;">1 hr ago</span></li>
</ul>
</div>
<div class="profile-card">
@@ -253,5 +432,6 @@
</ul>
</div>
</div>
</div>
@endsection
@endsection

File diff suppressed because it is too large Load Diff

View File

@@ -15,32 +15,44 @@
@endphp
<style>
/* [ALL YOUR ORIGINAL CSS HERE - SAME AS BEFORE] */
@keyframes fadeInUp {0% { transform: translateY(20px); opacity: 0; }100% { transform: translateY(0); opacity: 1; }}
.card, .custom-table-wrapper { animation: fadeInUp 0.8s ease both; }
.custom-table tbody tr { transition: all 0.25s ease-in-out; }
.custom-table tbody tr:hover { background-color: #fffbea; transform: scale(1.01); box-shadow: 0 2px 6px rgba(0,0,0,0.05); }
/* ✅ ROW HOVER EFFECT COMPLETELY DISABLED */
.custom-table tbody tr {
transition: none !important;
}
.custom-table tbody tr:hover {
background-color: transparent !important;
transform: none !important;
box-shadow: none !important;
}
.priority-badge {
display: inline-flex; align-items: center; font-size: 13.5px; padding: 6px 16px; border-radius: 12px; font-weight: 600;
box-shadow: 0 1px 2px 0 rgba(130,130,130,0.15); width: 90px; min-height: 28px; justify-content: center;
box-shadow: 0 1px 2px 0 rgba(230, 206, 206, 0.15); width: 90px; min-height: 28px; justify-content: center;
color: #fff; margin: 2px 0; transition: transform 0.2s ease-in-out;
}
.priority-badge:hover { transform: scale(1.08); }
.priority-high { background: linear-gradient(135deg, #ff8a8a, #d12929); }
.priority-medium { background: linear-gradient(135deg, #ffe390, #f5b041); }
.priority-low { background: linear-gradient(135deg, #b8f0c2, #1d8660); }
.custom-table thead th {
text-align: center; font-weight: 700; color: #000; padding: 14px; font-size: 17px; letter-spacing: 0.5px;
border-bottom: 2px solid #bfbfbf; background-color: #fde4b3;
text-align: center; font-weight: 700; color: #ffffffff; padding: 14px; font-size: 17px; letter-spacing: 0.5px;
border-bottom: 2px solid #bfbfbf; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.custom-table thead tr:first-child th:first-child { border-top-left-radius: 12px; }
.custom-table thead tr:first-child th:last-child { border-top-right-radius: 12px; }
.custom-table tbody tr:last-child td:first-child { border-bottom-left-radius: 12px; }
.custom-table tbody tr:last-child td:last-child { border-bottom-right-radius: 12px; }
.input-group input { border-radius: 10px 0 0 10px; border: 1px solid #ccc; box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); }
.input-group .btn { border-radius: 0 10px 10px 0; transition: all 0.2s ease-in-out; }
.input-group .btn:hover { background: #ffd65a; border-color: #ffd65a; color: #000; }
.card { border-radius: 16px; border: none; box-shadow: 0 4px 10px rgba(0,0,0,0.08); background: #fff; }
.badge {
font-size: 11px !important; font-weight: 600 !important; padding: 7px 13px !important; border-radius: 20px !important;
text-transform: uppercase; letter-spacing: 0.3px; display: inline-flex !important; align-items: center; justify-content: center;
@@ -54,8 +66,13 @@
@keyframes pulse {0% { box-shadow: 0 0 8px rgba(0,0,0,0.1); }50% { box-shadow: 0 0 14px rgba(0,0,0,0.15); }100% { box-shadow: 0 0 8px rgba(0,0,0,0.1); }}
.count-badge { --bs-badge-padding-x: 0.65em; --bs-badge-padding-y: 0.35em; --bs-badge-font-size: 0.75em; --bs-badge-font-weight: 700; --bs-badge-color: #fff; --bs-badge-border-radius: var(--bs-border-radius); }
h4.fw-bold { background: linear-gradient(90deg, #000000ff 0%, #030302ff 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: 800; letter-spacing: 1px; }
.custom-table tbody td:last-child { text-align: center !important; vertical-align: middle !important; }
.custom-table th,
.custom-table td {
text-align: center;
vertical-align: middle;
}
/* ===== PAGINATION STYLES ===== */
.pagination-container {
display: flex;
@@ -197,19 +214,19 @@
margin-bottom: 20px;
gap: 15px;
}
.search-form {
flex: 1;
max-width: 400px;
}
.search-input-group {
display: flex;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
border-radius: 10px;
overflow: hidden;
}
.search-input {
flex: 1;
border: 1px solid #e2e8f0;
@@ -219,13 +236,13 @@
background-color: #fff;
transition: all 0.3s ease;
}
.search-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.search-button {
background: #3b82f6;
border: 1px solid #3b82f6;
@@ -237,23 +254,23 @@
align-items: center;
justify-content: center;
}
.search-button:hover {
background: #2563eb;
border-color: #2563eb;
}
.search-icon {
width: 16px;
height: 16px;
}
.status-badges {
display: flex;
gap: 8px;
align-items: center;
}
.status-badge {
font-size: 12px;
font-weight: 600;
@@ -264,25 +281,34 @@
gap: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.status-badge-pending {
background: linear-gradient(135deg, #fef3c7, #fde68a);
color: #d97706;
border: 1px solid #f59e0b;
}
.status-badge-approved {
background: linear-gradient(135deg, #d1fae5, #a7f3d0);
color: #065f46;
border: 1px solid #10b981;
}
.status-badge-rejected {
background: linear-gradient(135deg, #fecaca, #fca5a5);
color: #991b1b;
border: 1px solid #ef4444;
}
/* ✅ TABLE WRAPPER REMOVE HORIZONTAL SCROLL */
.custom-table-wrapper {
overflow-x: hidden !important;
}
.custom-table {
width: 100%;
table-layout: auto;
}
/* Responsive styles */
@media (max-width: 768px) {
.pagination-container {
@@ -293,73 +319,64 @@
.pagination-controls {
justify-content: center;
}
.search-container {
flex-direction: column;
align-items: stretch;
}
.search-form {
max-width: 100%;
}
.status-badges {
justify-content: center;
}
}
/* ==============================================
PROFILE UPDATE REQUEST BUTTON BADGE FIX
============================================== */
/* Ensure button is positioning context */
a.btn.btn-primary.position-relative {
position: relative;
margin-right: 10px;
}
/* PROFILE UPDATE REQUEST BUTTON BADGE FIX */
a.btn.btn-primary.position-relative {
position: relative;
margin-right: 10px;
}
/* Fix badge inside Profile Update Requests button */
a.btn.btn-primary.position-relative .badge {
width: 30px !important;
height: 30px !important;
min-width: 30px !important;
padding: 0 !important;
font-size: 14px !important;
line-height: 30px !important;
border-radius: 50% !important;
display: inline-flex !important;
align-items: center;
justify-content: center;
animation: none !important;
box-shadow: 0 0 0 2px #ffffff;
}
a.btn.btn-primary.position-relative .badge {
width: 30px !important;
height: 30px !important;
min-width: 30px !important;
padding: 0 !important;
font-size: 14px !important;
line-height: 30px !important;
border-radius: 50% !important;
display: inline-flex !important;
align-items: center;
justify-content: center;
animation: none !important;
box-shadow: 0 0 0 2px #ffffff;
}
</style>
<!-- Counts -->
<div class="d-flex justify-content-between align-items-center mb-2 mt-3">
<h4 class="fw-bold mb-0">User Requests (Total: {{ $total }})</h4>
<h4 class="fw-bold mb-0">User Requests (Total: {{ $total }})</h4>
@can('request.update_profile')
<a href="{{ route('admin.profile.requests') }}" class="btn btn-primary position-relative">
<i class="bi bi-person-lines-fill me-1"></i>
Profile Update Requests
@if($pendingProfileUpdates > 0)
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
{{ $pendingProfileUpdates }}
</span>
@endif
</a>
@endcan
</div>
@can('request.update_profile')
<a href="{{ route('admin.profile.requests') }}" class="btn btn-primary position-relative">
<i class="bi bi-person-lines-fill me-1"></i>
Profile Update Requests
@if($pendingProfileUpdates > 0)
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
{{ $pendingProfileUpdates }}
</span>
@endif
</a>
@endcan
</div>
<!-- Search + Table -->
<div class="card mb-4 shadow-sm">
<div class="card-body pb-1">
<!-- Updated Search Bar with Status Badges in the same line -->
<div class="search-container">
<form method="GET" action="" class="search-form">
<div class="search-input-group">
@@ -371,7 +388,7 @@ a.btn.btn-primary.position-relative .badge {
</button>
</div>
</form>
<div class="status-badges">
<span class="status-badge status-badge-pending">{{ $requests->where('status', 'pending')->count() }} Pending</span>
<span class="status-badge status-badge-approved">{{ $requests->where('status', 'approved')->count() }} Approved</span>
@@ -381,9 +398,21 @@ a.btn.btn-primary.position-relative .badge {
<div class="table-responsive custom-table-wrapper">
<table class="table align-middle mb-0 custom-table">
<thead><tr>
<th>#</th><th>Request ID</th><th>Name</th><th>Company</th><th>Email</th><th>Mobile</th><th>Address</th><th>Priority</th><th>Date</th><th>Status</th><th>Actions</th>
</tr></thead>
<thead>
<tr>
<th>#</th>
<th>Request ID</th>
<th>Name</th>
<th>Company</th>
<th>Email</th>
<th>Mobile</th>
<th>Address</th>
<th>Priority</th>
<th>Date</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@forelse($currentItems as $index => $req)
<tr>
@@ -395,33 +424,41 @@ a.btn.btn-primary.position-relative .badge {
<td>{{ $req->mobile_no }}</td>
<td>{{ Str::limit($req->address, 30) }}</td>
<td>
@if(strtolower($req->priority) == 'high')<span class="priority-badge priority-high">High</span>
@elseif(strtolower($req->priority) == 'medium')<span class="priority-badge priority-medium">Medium</span>
@elseif(strtolower($req->priority) == 'low')<span class="priority-badge priority-low">Low</span>
@else{{ $req->priority ?? 'N/A' }}@endif
@if(strtolower($req->priority) == 'high')
<span class="priority-badge priority-high">High</span>
@elseif(strtolower($req->priority) == 'medium')
<span class="priority-badge priority-medium">Medium</span>
@elseif(strtolower($req->priority) == 'low')
<span class="priority-badge priority-low">Low</span>
@else
{{ $req->priority ?? 'N/A' }}
@endif
</td>
<td>{{ $req->date }}</td>
<td>
@if($req->status == 'approved')<span class="badge badge-approved"><i class="bi bi-check-circle-fill status-icon"></i>Approved</span>
@elseif($req->status == 'rejected')<span class="badge badge-rejected"><i class="bi bi-x-circle-fill status-icon"></i>Rejected</span>
@else<span class="badge badge-pending"><i class="bi bi-clock-fill status-icon"></i>Pending</span>@endif
@if($req->status == 'approved')
<span class="badge badge-approved"><i class="bi bi-check-circle-fill status-icon"></i>Approved</span>
@elseif($req->status == 'rejected')
<span class="badge badge-rejected"><i class="bi bi-x-circle-fill status-icon"></i>Rejected</span>
@else
<span class="badge badge-pending"><i class="bi bi-clock-fill status-icon"></i>Pending</span>
@endif
</td>
<td>
@if($req->status == 'pending')
<a href="{{ route('admin.requests.approve', $req->id) }}"
class="btn btn-success btn-sm">
<a href="{{ route('admin.requests.approve', $req->id) }}"
class="btn btn-success btn-sm">
<i class="bi bi-check-circle"></i> Approve
</a>
<a href="{{ route('admin.requests.reject', $req->id) }}"
class="btn btn-danger btn-sm">
<a href="{{ route('admin.requests.reject', $req->id) }}"
class="btn btn-danger btn-sm">
<i class="bi bi-x-circle"></i> Reject
</a>
@else
<span class="text-muted">No Action</span>
@endif
</td>
</tr>
@empty
<tr><td colspan="11" class="text-center text-muted py-4">No records found.</td></tr>
@@ -500,4 +537,4 @@ a.btn.btn-primary.position-relative .badge {
</div>
</div>
</div>
@endsection
@endsection

View File

@@ -4,6 +4,12 @@
@section('content')
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
overflow-x: hidden;
}
/* Hide scrollbar but keep scroll functionality */
html,

View File

@@ -4,6 +4,12 @@
@section('content')
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
overflow-x: hidden;
}
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);

View File

@@ -4,506 +4,465 @@
@section('content')
<style>
:root {
--primary: #4361ee;
--primary-dark: #3a56d4;
--secondary: #f72585;
--success: #4cc9f0;
--warning: #f8961e;
--danger: #e63946;
--light: #f8f9fa;
--dark: #212529;
--gray: #6c757d;
--border: #e2e8f0;
--card-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--hover-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
html, body {
margin: 0;
padding: 0;
width: 100%;
overflow-x: hidden;
}
:root {
--primary: #4361ee;
--primary-dark: #3a56d4;
--secondary: #f72585;
--success: #4cc9f0;
--warning: #f8961e;
--danger: #e63946;
--light: #f8f9fa;
--dark: #212529;
--gray: #6c757d;
--border: #e2e8f0;
--card-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--hover-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
/* Search Bar - Similar to Shipment */
/* Search Bar - Similar to Shipment */
.search-staff-bar {
display: flex;
align-items: center;
gap: 15px;
padding: 20px;
background: var(--gradient-primary);
border-radius: 16px;
box-shadow: var(--card-shadow);
flex-wrap: wrap;
margin-bottom: 30px;
color: white;
position: relative;
overflow: hidden;
}
.search-staff-bar::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255,255,255,0.1);
z-index: 0;
}
.search-staff-bar > * {
position: relative;
z-index: 1;
}
.search-staff-bar input,
.search-staff-bar select {
padding: 12px 16px;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 10px;
flex: 1;
min-width: 150px;
background: rgba(255,255,255,0.9);
font-weight: 500;
transition: all 0.3s ease;
color: var(--dark);
}
.search-staff-bar input:focus,
.search-staff-bar select:focus {
background: white;
box-shadow: 0 0 0 3px rgba(255,255,255,0.3);
outline: none;
}
.btn-add-staff {
background: rgba(255,255,255,0.2);
backdrop-filter: blur(10px);
color: white;
border: 1px solid rgba(255,255,255,0.3);
padding: 12px 24px;
border-radius: 10px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
white-space: nowrap;
font-weight: 600;
text-decoration: none;
}
.btn-add-staff:hover {
background: rgba(255,255,255,0.3);
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.search-icon {
font-size: 20px;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
}
.user-icon {
font-size: 18px;
}
@media (max-width: 768px) {
.search-staff-bar {
display: flex;
align-items: center;
gap: 15px;
padding: 20px;
background: var(--gradient-primary);
border-radius: 16px;
box-shadow: var(--card-shadow);
flex-wrap: wrap;
margin-bottom: 30px;
color: white;
position: relative;
overflow: hidden;
flex-direction: column;
align-items: stretch;
}
.search-staff-bar::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255,255,255,0.1);
z-index: 0;
}
.search-staff-bar > * {
position: relative;
z-index: 1;
}
.search-staff-bar input,
.search-staff-bar input,
.search-staff-bar select {
padding: 12px 16px;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 10px;
flex: 1;
min-width: 150px;
background: rgba(255,255,255,0.9);
font-weight: 500;
transition: all 0.3s ease;
color: var(--dark);
}
.search-staff-bar input:focus,
.search-staff-bar select:focus {
background: white;
box-shadow: 0 0 0 3px rgba(255,255,255,0.3);
outline: none;
}
.btn-add-staff {
background: rgba(255,255,255,0.2);
backdrop-filter: blur(10px);
color: white;
border: 1px solid rgba(255,255,255,0.3);
padding: 12px 24px;
border-radius: 10px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
white-space: nowrap;
font-weight: 600;
text-decoration: none;
}
.btn-add-staff:hover {
background: rgba(255,255,255,0.3);
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.search-icon {
font-size: 20px;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
}
.user-icon {
font-size: 18px;
}
@media (max-width: 768px) {
.search-staff-bar {
flex-direction: column;
align-items: stretch;
}
.search-staff-bar input,
.search-staff-bar select {
width: 100%;
}
}
/* Card Styles - Same as Shipment */
.card {
border: none;
border-radius: 16px;
box-shadow: var(--card-shadow);
transition: all 0.3s ease;
overflow: hidden;
}
.card:hover {
transform: translateY(-5px);
box-shadow: var(--hover-shadow);
}
.card-header {
background: var(--gradient-primary);
color: white;
border: none;
padding: 20px 25px;
border-radius: 16px 16px 0 0 !important;
}
.card-header h5 {
margin: 0;
font-weight: 700;
display: flex;
align-items: center;
gap: 10px;
}
/* Table Styles - Similar to Shipment */
.table-responsive {
border-radius: 0 0 16px 16px;
overflow-x: auto;
}
.table {
margin: 0;
border-collapse: separate;
border-spacing: 0;
width: 100%;
padding: 0;
}
.table thead th {
background: #f8f9fa;
border: none;
padding: 16px 12px;
font-weight: 700;
color: var(--dark);
text-align: left;
vertical-align: middle;
border-bottom: 2px solid var(--border);
position: relative;
}
.table tbody tr {
transition: all 0.3s ease;
}
.table tbody tr:hover {
background-color: #f8f9ff;
transform: scale(1.01);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.table tbody td {
padding: 14px 12px;
text-align: left;
vertical-align: middle;
border-bottom: 1px solid var(--border);
font-weight: 500;
}
.table tbody tr:last-child td {
border-bottom: none;
}
/* Status Badges - Similar Style */
.badge {
padding: 6px 12px !important;
border-radius: 20px !important;
font-weight: 600 !important;
font-size: 12px !important;
border: 2px solid transparent !important;
min-width: 80px !important;
text-align: center !important;
display: inline-block !important;
line-height: 1.2 !important;
}
.badge-active {
background: linear-gradient(135deg, #d1fae5, #a7f3d0) !important;
color: #065f46 !important;
border-color: #10b981 !important;
}
.badge-inactive {
background: linear-gradient(135deg, #fecaca, #fca5a5) !important;
color: #991b1b !important;
border-color: #ef4444 !important;
}
.badge-pending {
background: linear-gradient(135deg, #fef3c7, #fde68a) !important;
color: #92400e !important;
border-color: #f59e0b !important;
}
/* Employee ID Badge - Similar to Shipment ID */
.employee-id-badge {
font-family: 'Courier New', monospace;
background: rgba(67, 97, 238, 0.1);
padding: 4px 8px;
border-radius: 6px;
font-size: 0.85rem;
color: var(--primary);
border: 1px solid rgba(67, 97, 238, 0.2);
display: inline-block;
}
/* Action Buttons - Similar Style */
.action-buttons {
display: flex;
gap: 8px;
}
.btn-action {
padding: 6px 12px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 5px;
border: none;
cursor: pointer;
}
.btn-edit {
background: linear-gradient(135deg, #4cc9f0, #4361ee);
color: white;
}
.btn-edit:hover {
background: linear-gradient(135deg, #38bdf8, #3a56d4);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(76, 201, 240, 0.3);
}
.btn-delete {
background: linear-gradient(135deg, #f87171, #ef4444);
color: white;
}
.btn-delete:hover {
background: linear-gradient(135deg, #ef4444, #dc2626);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
/* Success Message - Similar Style */
.alert-success {
background: linear-gradient(135deg, #e6ffed, #d1f7e5);
border: 1px solid #b6f0c6;
border-left: 4px solid var(--success);
color: #0f5132;
padding: 1rem 1.25rem;
border-radius: 10px;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.alert-success:before {
content: '✓';
background: var(--success);
color: white;
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: var(--gray);
}
.empty-state:before {
content: '👤';
font-size: 3rem;
display: block;
margin-bottom: 1rem;
opacity: 0.5;
}
/* Role Badges */
.role-badge {
padding: 4px 8px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
background: rgba(67, 97, 238, 0.1);
color: var(--primary);
border: 1px solid rgba(67, 97, 238, 0.2);
}
/* Stats Cards - Similar to Shipment Totals */
}
/* Card Styles - Same as Shipment */
.card {
border: none;
border-radius: 16px;
box-shadow: var(--card-shadow);
transition: all 0.3s ease;
overflow: hidden;
}
.card:hover {
transform: translateY(-5px);
box-shadow: var(--hover-shadow);
}
.card-header {
background: var(--gradient-primary);
color: white;
border: none;
padding: 20px 25px;
border-radius: 16px 16px 0 0 !important;
}
.card-header h5 {
margin: 0;
font-weight: 700;
display: flex;
align-items: center;
gap: 10px;
}
/* Table Styles - Similar to Shipment */
.table-responsive {
border-radius: 0 0 16px 16px;
overflow-x: hidden; /* horizontal scroll remove */
}
.table {
margin: 0;
border-collapse: separate;
border-spacing: 0;
width: 100%;
padding: 0;
}
.table thead th {
background: #f8f9fa;
border: none;
padding: 16px 12px;
font-weight: 700;
color: var(--dark);
text-align: left;
vertical-align: middle;
border-bottom: 2px solid var(--border);
position: relative;
}
.table tbody tr {
transition: all 0.3s ease;
}
/* hover stable ठेवण्यासाठी */
.table tbody tr:hover {
background-color: inherit;
transform: none;
box-shadow: none;
}
.table tbody td {
padding: 14px 12px;
text-align: left;
vertical-align: middle;
border-bottom: 1px solid var(--border);
font-weight: 500;
}
.table tbody tr:last-child td {
border-bottom: none;
}
/* Status Badges */
.badge {
padding: 6px 12px !important;
border-radius: 20px !important;
font-weight: 600 !important;
font-size: 12px !important;
border: 2px solid transparent !important;
min-width: 80px !important;
text-align: center !important;
display: inline-block !important;
line-height: 1.2 !important;
}
.badge-active {
background: linear-gradient(135deg, #d1fae5, #a7f3d0) !important;
color: #065f46 !important;
border-color: #10b981 !important;
}
.badge-inactive {
background: linear-gradient(135deg, #fecaca, #fca5a5) !important;
color: #991b1b !important;
border-color: #ef4444 !important;
}
.badge-pending {
background: linear-gradient(135deg, #fef3c7, #fde68a) !important;
color: #92400e !important;
border-color: #f59e0b !important;
}
/* Employee ID Badge */
.employee-id-badge {
font-family: 'Courier New', monospace;
background: rgba(67, 97, 238, 0.1);
padding: 4px 8px;
border-radius: 6px;
font-size: 0.85rem;
color: var(--primary);
border: 1px solid rgba(67, 97, 238, 0.2);
display: inline-block;
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: 8px;
}
.btn-action {
padding: 6px 12px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 5px;
border: none;
cursor: pointer;
}
.btn-edit {
background: linear-gradient(135deg, #4cc9f0, #4361ee);
color: white;
}
.btn-edit:hover {
background: linear-gradient(135deg, #38bdf8, #3a56d4);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(76, 201, 240, 0.3);
}
.btn-delete {
background: linear-gradient(135deg, #f87171, #ef4444);
color: white;
}
.btn-delete:hover {
background: linear-gradient(135deg, #ef4444, #dc2626);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
/* Success Message */
.alert-success {
background: linear-gradient(135deg, #e6ffed, #d1f7e5);
border: 1px solid #b6f0c6;
border-left: 4px solid var(--success);
color: #0f5132;
padding: 1rem 1.25rem;
border-radius: 10px;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.alert-success:before {
content: '✓';
background: var(--success);
color: white;
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: var(--gray);
}
.empty-state:before {
content: '👤';
font-size: 3rem;
display: block;
margin-bottom: 1rem;
opacity: 0.5;
}
/* Role Badges */
.role-badge {
padding: 4px 8px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
background: rgba(67, 97, 238, 0.1);
color: var(--primary);
border: 1px solid rgba(67, 97, 238, 0.2);
}
/* Stats Cards */
.stats-cards {
display: flex;
gap: 20px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.stat-card {
flex: 1;
min-width: 200px;
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: var(--card-shadow);
display: flex;
align-items: center;
gap: 15px;
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-3px);
box-shadow: var(--hover-shadow);
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.stat-icon.total {
background: linear-gradient(135deg, #e6f3ff, #c2d9ff);
color: var(--primary);
}
.stat-icon.active {
background: linear-gradient(135deg, #d1fae5, #a7f3d0);
color: #10b981;
}
.stat-content h3 {
font-size: 1.8rem;
font-weight: 700;
margin: 0;
color: var(--dark);
}
.stat-content p {
color: var(--gray);
margin: 4px 0 0 0;
font-size: 0.875rem;
}
/* Pagination - Same as Shipment */
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15px;
padding: 12px 25px;
border-top: 1px solid #eef3fb;
}
.pagination-info {
font-size: 13px;
color: #9ba5bb;
font-weight: 600;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 8px;
}
.pagination-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;
align-items: center;
justify-content: center;
min-width: 40px;
height: 32px;
}
.pagination-btn:hover:not(:disabled) {
background: #1a2951;
color: white;
border-color: #1a2951;
}
.pagination-btn:disabled {
background: #f8fafc;
color: #cbd5e0;
border-color: #e2e8f0;
cursor: not-allowed;
opacity: 0.6;
}
.pagination-page-btn {
background: #fff;
border: 1px solid #e3eaf6;
color: #1a2951;
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
min-width: 36px;
text-align: center;
}
.pagination-page-btn:hover {
background: #1a2951;
color: white;
border-color: #1a2951;
}
.pagination-page-btn.active {
background: #1a2951;
color: white;
border-color: #1a2951;
}
.pagination-pages {
display: flex;
gap: 4px;
align-items: center;
}
.pagination-ellipsis {
color: #9ba5bb;
font-size: 13px;
padding: 0 4px;
}
@media (max-width: 768px) {
.stats-cards {
display: flex;
gap: 20px;
margin-bottom: 30px;
flex-wrap: wrap;
flex-direction: column;
}
.stat-card {
flex: 1;
min-width: 200px;
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: var(--card-shadow);
display: flex;
align-items: center;
gap: 15px;
transition: all 0.3s ease;
min-width: 100%;
}
.stat-card:hover {
transform: translateY(-3px);
box-shadow: var(--hover-shadow);
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.stat-icon.total {
background: linear-gradient(135deg, #e6f3ff, #c2d9ff);
color: var(--primary);
}
.stat-icon.active {
background: linear-gradient(135deg, #d1fae5, #a7f3d0);
color: #10b981;
}
.stat-content h3 {
font-size: 1.8rem;
font-weight: 700;
margin: 0;
color: var(--dark);
}
.stat-content p {
color: var(--gray);
margin: 4px 0 0 0;
font-size: 0.875rem;
}
/* Pagination - Same as Shipment */
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15px;
padding: 12px 25px;
border-top: 1px solid #eef3fb;
flex-direction: column;
gap: 10px;
align-items: stretch;
}
.pagination-info {
font-size: 13px;
color: #9ba5bb;
font-weight: 600;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 8px;
}
.pagination-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;
align-items: center;
justify-content: center;
min-width: 40px;
height: 32px;
}
.pagination-btn:hover:not(:disabled) {
background: #1a2951;
color: white;
border-color: #1a2951;
}
.pagination-btn:disabled {
background: #f8fafc;
color: #cbd5e0;
border-color: #e2e8f0;
cursor: not-allowed;
opacity: 0.6;
}
.pagination-page-btn {
background: #fff;
border: 1px solid #e3eaf6;
color: #1a2951;
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
min-width: 36px;
text-align: center;
}
.pagination-page-btn:hover {
background: #1a2951;
color: white;
border-color: #1a2951;
}
.pagination-page-btn.active {
background: #1a2951;
color: white;
border-color: #1a2951;
}
.pagination-pages {
display: flex;
gap: 4px;
align-items: center;
}
.pagination-ellipsis {
color: #9ba5bb;
font-size: 13px;
padding: 0 4px;
}
@media (max-width: 768px) {
.stats-cards {
flex-direction: column;
}
.stat-card {
min-width: 100%;
}
.pagination-container {
flex-direction: column;
gap: 10px;
align-items: stretch;
}
.pagination-controls {
justify-content: center;
}
}
}
</style>
<div class="container-fluid py-4">
@if(session('success'))
<div class="alert-success">
{{ session('success') }}
@@ -716,7 +675,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Search filter
if (searchTerm) {
const matchesSearch =
const matchesSearch =
staff.name.toLowerCase().includes(searchTerm) ||
staff.email.toLowerCase().includes(searchTerm) ||
(staff.employee_id && staff.employee_id.toLowerCase().includes(searchTerm)) ||
@@ -893,4 +852,4 @@ function renderTable() {
});
}
</script>
@endsection
@endsection

View File

@@ -1,36 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Laravel Test Page</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
/* Paste the provided CSS here */
</style>
</head>
<body class="bg-light">
<div class="container mt-5">
<div class="card shadow-lg border-0 rounded-4">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">Laravel Gitea Test Page</h4>
</div>
<div class="card-body">
<h5 class="text-success"> Laravel is running successfully!</h5>
<p class="mt-3">If you see this page, your Blade views and routing work perfectly.</p>
<ul class="list-group mt-4">
<li class="list-group-item"><strong>PHP Version:</strong> {{ PHP_VERSION }}</li>
<li class="list-group-item"><strong>Laravel Version:</strong> {{ app()->version() }}</li>
<li class="list-group-item"><strong>Environment:</strong> {{ app()->environment() }}</li>
</ul>
<a href="{{ url('/') }}" class="btn btn-outline-primary mt-4">Go to Home testing done</a>
</div>
</div>
</div>
</body>
</html>

View File

@@ -12,48 +12,66 @@ use App\Http\Controllers\Admin\AdminAccountController;
use App\Http\Controllers\Admin\AdminReportController;
use App\Http\Controllers\Admin\AdminStaffController;
use App\Http\Controllers\Admin\AdminChatController;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Broadcast;
use App\Http\Controllers\ContainerController;
// ---------------------------
// Public Front Page
// ---------------------------
Route::get('/', function () {
return view('welcome');
return redirect()->route('admin.login');
});
// ==========================================
// BROADCASTING AUTH (FOR ADMIN SESSION)
// ==========================================
Broadcast::routes(['middleware' => ['web']]);
Route::post('/broadcasting/auth', function (\Illuminate\Http\Request $request) {
\Log::info('🎯 Broadcasting Auth Request', [
'channel' => $request->input('channel_name'),
'admin_check' => auth('admin')->check(),
'web_check' => auth('web')->check(),
]);
if (auth('admin')->check()) {
\Log::info('✅ Admin authenticated', ['id' => auth('admin')->id()]);
return Broadcast::auth($request);
}
if (auth('web')->check()) {
\Log::info('✅ Web user authenticated', ['id' => auth('web')->id()]);
return Broadcast::auth($request);
}
\Log::warning('❌ No authentication found');
return response()->json(['message' => 'Unauthenticated'], 403);
})->middleware('web');
// ---------------------------
// ADMIN LOGIN ROUTES
// ---------------------------
// login routes (public)
Route::prefix('admin')->middleware('web')->group(function () {
Route::prefix('admin')->group(function () {
Route::get('/login', [AdminAuthController::class, 'showLoginForm'])->name('admin.login');
Route::post('/login', [AdminAuthController::class, 'login'])->name('admin.login.submit');
Route::post('/logout', [AdminAuthController::class, 'logout'])->name('admin.logout');
});
Route::get('/login', function () {
return redirect()->route('admin.login');
})->name('login');
Broadcast::routes(['middleware' => ['web']]);
// ==========================================
// PROTECTED ADMIN ROUTES (session protected)
// ==========================================
Route::prefix('admin')
->middleware(['web', 'auth:admin'])
->middleware('auth:admin')
->group(function () {
// Dashboard
Route::get('/dashboard', [AdminOrderController::class, 'index'])
Route::get('/dashboard', [AdminOrderController::class, 'dashboard'])
->name('admin.dashboard');
Route::get('/reports', [AdminReportController::class, 'index'])->name('admin.reports');
Route::get('/reports', [AdminReportController::class, 'index'])->name('admin.reports');
Route::get('/chat-support', fn() => view('admin.chat_support'))
->name('admin.chat_support');
@@ -64,294 +82,298 @@ Route::prefix('admin')
Route::get('/account', fn() => view('admin.account'))
->name('admin.account');
Route::get('/profile', fn() => view('admin.profile'))
->name('admin.profile');
Route::get('/profile', [AdminAuthController::class, 'profile'])->name('admin.profile');
Route::post(
'admin/orders/upload-excel-preview',
[\App\Http\Controllers\Admin\AdminOrderController::class, 'uploadExcelPreview']
)->name('admin.orders.upload.excel.preview');
//---------------------------
// CONTAINER ROUTES
//---------------------------
Route::get('/containers', [ContainerController::class, 'index'])
->name('containers.index');
Route::get('/containers/create', [ContainerController::class, 'create'])
->name('containers.create');
Route::post('/containers', [ContainerController::class, 'store'])
->name('containers.store');
Route::get('/containers/{container}', [ContainerController::class, 'show'])
->name('containers.show');
Route::post('/containers/{container}/update-rows', [ContainerController::class, 'updateRows'])
->name('containers.rows.update');
Route::post('containers/{container}/status', [ContainerController::class, 'updateStatus'])
->name('containers.update-status');
Route::delete('/containers/{container}', [ContainerController::class, 'destroy'])
->name('containers.destroy');
Route::get('containers/{container}/download-pdf', [ContainerController::class, 'downloadPdf'])
->name('containers.download.pdf');
Route::get('/admin/containers/{container}/download-excel', [ContainerController::class, 'downloadExcel'])
->name('containers.download.excel');
// ---------------------------
// USER REQUESTS
// ---------------------------
Route::get('/requests', [UserRequestController::class, 'index'])
->name('admin.requests');
Route::get('/requests', [UserRequestController::class, 'index'])
->name('admin.requests');
Route::get('/requests/approve/{id}', [UserRequestController::class, 'approve'])
->name('admin.requests.approve');
Route::get('/requests/approve/{id}', [UserRequestController::class, 'approve'])
->name('admin.requests.approve');
Route::get('/requests/reject/{id}', [UserRequestController::class, 'reject'])
->name('admin.requests.reject');
Route::get('/requests/reject/{id}', [UserRequestController::class, 'reject'])
->name('admin.requests.reject');
// PROFILE UPDATE REQUESTS
Route::get('/profile-update-requests',
Route::get('/profile-update-requests',
[UserRequestController::class, 'profileUpdateRequests']
)->name('admin.profile.requests');
Route::get('/profile-update/approve/{id}',
Route::get('/profile-update/approve/{id}',
[UserRequestController::class, 'approveProfileUpdate']
)->name('admin.profile.approve');
Route::get('/profile-update/reject/{id}',
Route::get('/profile-update/reject/{id}',
[UserRequestController::class, 'rejectProfileUpdate']
)->name('admin.profile.reject');
// ---------------------------
// MARK LIST
// ---------------------------
Route::get('/mark-list', [AdminMarkListController::class, 'index'])
->name('admin.marklist.index');
Route::get('/mark-list/status/{id}', [AdminMarkListController::class, 'toggleStatus'])
->name('admin.marklist.toggle');
Route::get('/mark-list', [AdminMarkListController::class, 'index'])
->name('admin.marklist.index');
Route::get('/mark-list/status/{id}', [AdminMarkListController::class, 'toggleStatus'])
->name('admin.marklist.toggle');
// ---------------------------
// ORDERS
// ORDERS (UPDATED)
// ---------------------------
Route::get('/orders', [AdminOrderController::class, 'orderShow'])
->name('admin.orders');
Route::get('/orders', [AdminOrderController::class, 'index'])
->name('admin.orders');
Route::get('/orders/list', [AdminOrderController::class, 'index'])
->name('admin.orders.index');
Route::get('/orders/list', [AdminOrderController::class, 'index'])
->name('admin.orders.index');
Route::get('/orders/{id}', [AdminOrderController::class, 'show'])
->name('admin.orders.show');
Route::get('/orders/{id}', [AdminOrderController::class, 'show'])
->name('admin.orders.show');
Route::post('/orders/temp/add', [AdminOrderController::class, 'addTempItem'])
->name('admin.orders.temp.add');
Route::post('/orders/temp/add', [AdminOrderController::class, 'addTempItem'])
->name('admin.orders.temp.add');
Route::post('/orders/temp/delete', [AdminOrderController::class, 'deleteTempItem'])
->name('admin.orders.temp.delete');
Route::post('/orders/temp/delete', [AdminOrderController::class, 'deleteTempItem'])
->name('admin.orders.temp.delete');
Route::post('/orders/temp/reset', [AdminOrderController::class, 'resetTemp'])
->name('admin.orders.temp.reset');
Route::post('/orders/temp/reset', [AdminOrderController::class, 'resetTemp'])
->name('admin.orders.temp.reset');
Route::post('/orders/finish', [AdminOrderController::class, 'finishOrder'])
->name('admin.orders.finish');
Route::post('/orders/finish', [AdminOrderController::class, 'finishOrder'])
->name('admin.orders.finish');
Route::get('/orders/view/{id}', [AdminOrderController::class, 'popup'])
->name('admin.orders.popup');
Route::get('/orders/view/{id}', [AdminOrderController::class, 'popup'])
->name('admin.orders.popup');
Route::get('/orders/{id}/see', [AdminOrderController::class, 'see'])
->name('orders.see');
Route::post('/admin/orders/temp/add', [AdminOrderController::class, 'addTempItem'])
->name('admin.orders.temp.add');
// Route::get('/orders/{id}', [AdminOrderController::class, 'view'])
// ->name('admin.orders.view');
Route::get('/orders/{order:order_id}/see', [AdminOrderController::class, 'see'])
->name('admin.orders.see');
// ---------------------------
// ORDERS (FIXED ROUTES)
// ---------------------------
// Add item to order
Route::post('/orders/{order}/item', [AdminOrderController::class, 'addItem'])
->name('admin.orders.addItem');
// Delete item from order
Route::delete('/orders/item/{id}', [AdminOrderController::class, 'deleteItem'])
->name('admin.orders.deleteItem');
// Restore deleted item
Route::post('/orders/item/{id}/restore', [AdminOrderController::class, 'restoreItem'])
->name('admin.orders.restoreItem');
// Update main order fields
Route::put('/admin/orders/item/update/{id}', [AdminOrderController::class, 'updateItem'])
Route::post('/orders/{order}/item', [AdminOrderController::class, 'addItem'])
->name('admin.orders.addItem');
Route::delete('/orders/item/{id}', [AdminOrderController::class, 'deleteItem'])
->name('admin.orders.deleteItem');
Route::post('/orders/item/{id}/restore', [AdminOrderController::class, 'restoreItem'])
->name('admin.orders.restoreItem');
Route::put('/orders/item/update/{id}', [AdminOrderController::class, 'updateItem'])
->name('admin.orders.updateItem');
// Delete full order
Route::delete('/orders/{id}/delete', [AdminOrderController::class, 'destroy'])
->name('admin.orders.destroy');
Route::delete('/orders/{id}/delete', [AdminOrderController::class, 'destroy'])
->name('admin.orders.destroy');
Route::post('/orders/upload-excel-preview',
[AdminOrderController::class, 'uploadExcelPreview']
)->name('admin.orders.upload.excel.preview');
// ---------------------------
// SHIPMENTS (FIXED ROUTES)
// ---------------------------
Route::get('/shipments', [ShipmentController::class, 'index'])
->name('admin.shipments');
// ---------------------------
// SHIPMENTS (FIXED ROUTES)
// ---------------------------
Route::get('/shipments', [ShipmentController::class, 'index'])
->name('admin.shipments');
Route::post('/shipments', [ShipmentController::class, 'store'])
->name('admin.shipments.store');
Route::post('/shipments/update-status', [ShipmentController::class, 'updateStatus'])
->name('admin.shipments.updateStatus');
// Get shipment orders for modal (AJAX)
Route::get('/shipments/{id}/orders', [ShipmentController::class, 'getShipmentOrders'])
->name('admin.shipments.orders');
// Get shipment details for edit (AJAX)
Route::get('/shipments/{id}', [ShipmentController::class, 'show'])
->name('admin.shipments.show');
// Shipment Update
Route::put('/shipments/{id}', [ShipmentController::class, 'update'])
->name('admin.shipments.update');
// Shipment Delete
Route::delete('/shipments/{id}', [ShipmentController::class, 'destroy'])
->name('admin.shipments.destroy');
Route::get('/shipment/dummy/{id}', [ShipmentController::class, 'dummy'])
->name('admin.shipments.dummy');
Route::post('/shipments', [ShipmentController::class, 'store'])
->name('admin.shipments.store');
// web.php
Route::delete('/shipments/{shipment}/orders/{order}',
[ShipmentController::class, 'removeOrder']
)->name('admin.shipments.removeOrder');
Route::post('/shipments/{shipment}/add-orders',
[ShipmentController::class, 'addOrders']
)->name('admin.shipments.addOrders');
Route::get('/shipment/dummy/{id}', [ShipmentController::class, 'dummy'])
->name('admin.shipments.dummy');
Route::post('/shipments/update-status', [ShipmentController::class, 'updateStatus'])
->name('admin.shipments.updateStatus');
Route::get('/shipments/{id}/orders', [ShipmentController::class, 'getShipmentOrders'])
->name('admin.shipments.orders');
Route::get('/shipments/{id}', [ShipmentController::class, 'show'])
->name('admin.shipments.show');
Route::put('/shipments/{id}', [ShipmentController::class, 'update'])
->name('admin.shipments.update');
Route::delete('/shipments/{id}', [ShipmentController::class, 'destroy'])
->name('admin.shipments.destroy');
Route::get('/shipment/dummy/{id}', [ShipmentController::class, 'dummy'])
->name('admin.shipments.dummy');
// ---------------------------
// INVOICES
// ---------------------------
Route::get('/invoices', [AdminInvoiceController::class, 'index'])
->name('admin.invoices.index');
Route::get('/invoices', [AdminInvoiceController::class, 'index'])
->name('admin.invoices.index');
Route::get('/invoices/{id}/popup', [AdminInvoiceController::class, 'popup'])
->name('admin.invoices.popup');
Route::get('/invoices/{id}/popup', [AdminInvoiceController::class, 'popup'])
->name('admin.invoices.popup');
Route::get('/invoices/{id}/edit', [AdminInvoiceController::class, 'edit'])
->name('admin.invoices.edit');
Route::get('/invoices/{id}/edit', [AdminInvoiceController::class, 'edit'])
->name('admin.invoices.edit');
Route::post('/invoices/{id}/update', [AdminInvoiceController::class, 'update'])
->name('admin.invoices.update');
Route::post('/invoices/{id}/update', [AdminInvoiceController::class, 'update'])
->name('admin.invoices.update');
Route::post('/invoices/{invoice}/installments', [AdminInvoiceController::class, 'storeInstallment'])
->name('admin.invoice.installment.store');
Route::post('/invoices/{id}/installment', [AdminInvoiceController::class, 'storeInstallment'])
Route::post('/invoices/{invoice}/installments', [AdminInvoiceController::class, 'storeInstallment'])
->name('admin.invoice.installment.store');
Route::get(
'/admin/invoices/{id}/download',
[AdminInvoiceController::class, 'downloadInvoice']
)->name('admin.invoices.download');
Route::post('/invoices/{id}/installment', [AdminInvoiceController::class, 'storeInstallment'])
->name('admin.invoice.installment.store');
Route::delete('/installment/{id}', [AdminInvoiceController::class, 'deleteInstallment'])
->name('admin.invoice.installment.delete');
->name('admin.invoice.installment.delete');
Route::put('admin/invoices/{invoice}/items', [AdminInvoiceController::class, 'updateItems'])
->name('admin.invoices.items.update');
// //Add New Invoice
Route::get('/admin/invoices/create', [InvoiceController::class, 'create'])->name('admin.invoices.create');
Route::post('/admin/invoices/{invoice}/charge-group', [AdminInvoiceController::class, 'storeChargeGroup'])
->name('admin.invoices.charge-group.store');
// जर create page वापरायचा असेल तर AdminInvoiceController मध्ये create() असावा.
// नसेल तर हा route comment / delete कर.
// Route::get('/admin/invoices/create', [AdminInvoiceController::class, 'create'])
// ->name('admin.invoices.create');
// ---------------------------
// CUSTOMERS
// ---------------------------
Route::get('/customers', [AdminCustomerController::class, 'index'])
->name('admin.customers.index');
Route::get('/customers', [AdminCustomerController::class, 'index'])
->name('admin.customers.index');
Route::get('/customers/add', [AdminCustomerController::class, 'create'])
->name('admin.customers.add');
Route::get('/customers/add', [AdminCustomerController::class, 'create'])
->name('admin.customers.add');
Route::post('/customers/store', [AdminCustomerController::class, 'store'])
->name('admin.customers.store');
Route::post('/customers/store', [AdminCustomerController::class, 'store'])
->name('admin.customers.store');
Route::get('/customers/{id}/view', [AdminCustomerController::class, 'view'])
->name('admin.customers.view');
Route::get('/customers/{id}/view', [AdminCustomerController::class, 'view'])
->name('admin.customers.view');
Route::post('/customers/{id}/status', [AdminCustomerController::class, 'toggleStatus'])
->name('admin.customers.status');
Route::post('/customers/{id}/status', [AdminCustomerController::class, 'toggleStatus'])
->name('admin.customers.status');
// CHAT
Route::get('/chat-support', [AdminChatController::class, 'index'])
->name('admin.chat_support');
// Chat list page
Route::get('/chat-support', [AdminChatController::class, 'index'])
->name('admin.chat_support');
Route::get('/chat-support/{ticketId}', [AdminChatController::class, 'openChat'])
->name('admin.chat.open');
// Chat window (open specific user's chat)
Route::get('/chat-support/{ticketId}', [AdminChatController::class, 'openChat'])
->name('admin.chat.open');
// Admin sending message
Route::post('/chat-support/{ticketId}/send', [AdminChatController::class, 'sendMessage'])
->name('admin.chat.send');
});
// ==========================================
// ADMIN ACCOUNT (AJAX) ROUTES
// ==========================================
Route::prefix('admin/account')
->middleware(['web', 'auth:admin'])
->name('admin.account.')
->group(function () {
Route::get('/dashboard', [AdminAccountController::class, 'getDashboardData'])
->name('dashboard');
Route::get('/available-orders', [AdminAccountController::class, 'getAvailableOrders'])
->name('orders.available');
Route::post('/create-order', [AdminAccountController::class, 'accountCreateOrder'])
->name('create');
Route::post('/toggle-payment', [AdminAccountController::class, 'togglePayment'])
->name('toggle');
Route::post('/installment/create', [AdminAccountController::class, 'addInstallment'])
->name('installment.create');
Route::post('/installment/update-status', [AdminAccountController::class, 'updateInstallmentStatus'])
->name('installment.update');
Route::get('/entry/{entry_no}', [AdminAccountController::class, 'getEntryDetails'])
->name('entry.details');
// ⬇⬇ NEW ROUTES FOR EDIT + DELETE ⬇⬇
Route::post('/update-entry', [AdminAccountController::class, 'updateEntry'])
->name('update.entry');
Route::post('/delete-entry', [AdminAccountController::class, 'deleteEntry'])
->name('delete.entry');
// ===== Associated Orders Routes (EDIT MODAL साठी) =====
Route::post('/add-orders-to-entry', [AdminAccountController::class, 'addOrdersToEntry'])
->name('add.orders.to.entry');
Route::get('/entry-orders/{entry_no}', [AdminAccountController::class, 'getEntryOrders'])
->name('entry.orders');
Route::post('/remove-order-from-entry', [AdminAccountController::class, 'removeOrderFromEntry'])
->name('remove.order.from.entry');
});
// ---------------------------
// REPORTS DOWNLOAD ROUTES
// ---------------------------
Route::get('/admin/orders/download/pdf', [AdminOrderController::class, 'downloadPdf'])
->name('admin.orders.download.pdf');
Route::get('/admin/orders/download/excel', [AdminOrderController::class, 'downloadExcel'])
->name('admin.orders.download.excel');
Route::prefix('admin/account')->middleware(['web', 'auth:admin'])->name('admin.account.')->group(function () {
Route::post('/toggle-payment', [AdminAccountController::class, 'togglePayment'])->name('toggle');
});
//---------------------------
//Edit Button Route
//---------------------------
// protected admin routes
Route::middleware(['web', 'auth:admin'])
->prefix('admin')
->name('admin.')
->group(function () {
// staff resource
Route::resource('staff', AdminStaffController::class);
Route::post('/chat-support/{ticketId}/send', [AdminChatController::class, 'sendMessage'])
->name('admin.chat.send');
});
// ==========================================
// ADMIN ACCOUNT (AJAX) ROUTES
// ==========================================
Route::prefix('admin/account')
->middleware('auth:admin')
->name('admin.account.')
->group(function () {
Route::get('/dashboard', [AdminAccountController::class, 'getDashboardData'])
->name('dashboard');
Route::get('/available-orders', [AdminAccountController::class, 'getAvailableOrders'])
->name('orders.available');
Route::post('/create-order', [AdminAccountController::class, 'accountCreateOrder'])
->name('create');
Route::post('/toggle-payment', [AdminAccountController::class, 'togglePayment'])
->name('toggle');
Route::post('/installment/create', [AdminAccountController::class, 'addInstallment'])
->name('installment.create');
Route::post('/installment/update-status', [AdminAccountController::class, 'updateInstallmentStatus'])
->name('installment.update');
Route::get('/entry/{entry_no}', [AdminAccountController::class, 'getEntryDetails'])
->name('entry.details');
Route::post('/update-entry', [AdminAccountController::class, 'updateEntry'])
->name('update.entry');
Route::post('/delete-entry', [AdminAccountController::class, 'deleteEntry'])
->name('delete.entry');
Route::post('/add-orders-to-entry', [AdminAccountController::class, 'addOrdersToEntry'])
->name('add.orders.to.entry');
Route::get('/entry-orders/{entry_no}', [AdminAccountController::class, 'getEntryOrders'])
->name('entry.orders');
Route::post('/remove-order-from-entry', [AdminAccountController::class, 'removeOrderFromEntry'])
->name('remove.order.from.entry');
});
// ---------------------------
// REPORTS DOWNLOAD ROUTES
// ---------------------------
Route::get('/admin/orders/download/pdf', [AdminOrderController::class, 'downloadPdf'])
->name('admin.orders.download.pdf');
Route::get('/admin/orders/download/excel', [AdminOrderController::class, 'downloadExcel'])
->name('admin.orders.download.excel');
Route::prefix('admin/account')->middleware('auth:admin')->name('admin.account.')->group(function () {
Route::post('/toggle-payment', [AdminAccountController::class, 'togglePayment'])->name('toggle');
});
// STAFF RESOURCE
Route::middleware(['auth:admin'])
->prefix('admin')
->name('admin.')
->group(function () {
Route::resource('staff', AdminStaffController::class);
});
// Extra admin prefix group
Route::prefix('admin')->middleware('auth:admin')->group(function () {
// ... your routes
});
Route::post('/admin/broadcasting/auth', function () {
return Broadcast::auth(request());
})->middleware('auth:admin');
// INVOICE DOWNLOAD (AdminInvoiceController वापरून)
Route::get('/admin/invoices/{invoice}/download', [AdminInvoiceController::class, 'download'])
->name('admin.invoices.download');
// CONTAINER POPUP VIEW
// In admin group
Route::get('/admin/containers/{container}/popup', [\App\Http\Controllers\ContainerController::class, 'popupPopup'])
->name('containers.popup');