Compare commits
46 Commits
8a0d122e2c
...
dev
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
19d7f423b3 | ||
|
|
0257b68f16 | ||
|
|
785f2564be | ||
|
|
9a6ca49ad7 | ||
|
|
bf2689e62d | ||
|
|
c25b468c77 | ||
|
|
5d8a746876 | ||
|
|
bb2a361a97 | ||
|
|
6b5876e08f | ||
|
|
43b1a64911 | ||
|
|
ff4c006ca4 | ||
|
|
d5e9113820 | ||
|
|
bddbcf5c5f | ||
|
|
0c51ed1489 | ||
|
|
9cc6959396 | ||
|
|
c11467068c | ||
|
|
599023166a | ||
|
|
e188780329 | ||
|
|
338425535e | ||
|
|
f7856a6755 | ||
|
|
8f95091673 | ||
|
|
7362ef6bdc | ||
|
|
e872b83ea3 | ||
|
|
6ccf2cf84e | ||
|
|
4637f0b189 | ||
|
|
952dd7eddd | ||
|
|
451be1a533 | ||
|
|
cd9a786ef4 | ||
|
|
a6dd919d3f | ||
|
|
e0a8a5c69c | ||
|
|
7fa03688aa | ||
|
|
1885d3beef | ||
|
|
72a81fa111 | ||
|
|
044bfe5563 | ||
|
|
8ca8f05b93 | ||
|
|
2741129740 | ||
|
|
1bce2be826 | ||
|
|
ea2532efc8 | ||
|
|
ccce02f43e | ||
|
|
cdb6cab57d | ||
|
|
3941b06355 | ||
|
|
d2730e78f6 | ||
|
|
80c6e42e0c | ||
|
|
e455c271c4 | ||
|
|
48f7ab82ff | ||
|
|
c4097ecbde |
14
.env.example
14
.env.example
@@ -1,6 +1,6 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_KEY=base64:/ulhRgiCOFjZV6xUDkXLfiR9X8iFRZ4QIiX3UJbdwY4=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
@@ -20,12 +20,12 @@ LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=kent_logistics6
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
63
app/Exports/InvoicesExport.php
Normal file
63
app/Exports/InvoicesExport.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Maatwebsite\Excel\Concerns\FromView;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class InvoicesExport implements FromView
|
||||
{
|
||||
protected $request;
|
||||
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function view(): View
|
||||
{
|
||||
$request = $this->request;
|
||||
|
||||
$invoices = DB::table('invoices')
|
||||
->leftJoin('containers', 'containers.id', '=', 'invoices.container_id')
|
||||
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
|
||||
->select(
|
||||
'invoices.invoice_number',
|
||||
'invoices.invoice_date',
|
||||
'invoices.mark_no',
|
||||
'containers.container_number',
|
||||
'containers.container_date',
|
||||
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
|
||||
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name'),
|
||||
'invoices.final_amount',
|
||||
'invoices.final_amount_with_gst',
|
||||
'invoices.status as invoice_status'
|
||||
)
|
||||
->when($request->filled('search'), function ($q) use ($request) {
|
||||
$search = trim($request->search);
|
||||
$q->where(function ($qq) use ($search) {
|
||||
$qq->where('invoices.invoice_number', 'like', "%{$search}%")
|
||||
->orWhere('invoices.mark_no', 'like', "%{$search}%")
|
||||
->orWhere('containers.container_number', 'like', "%{$search}%")
|
||||
->orWhere('mark_list.company_name', 'like', "%{$search}%")
|
||||
->orWhere('mark_list.customer_name', 'like', "%{$search}%");
|
||||
});
|
||||
})
|
||||
->when($request->filled('status'), function ($q) use ($request) {
|
||||
$q->where('invoices.status', $request->status);
|
||||
})
|
||||
->when($request->filled('from_date'), function ($q) use ($request) {
|
||||
$q->whereDate('invoices.invoice_date', '>=', $request->from_date);
|
||||
})
|
||||
->when($request->filled('to_date'), function ($q) use ($request) {
|
||||
$q->whereDate('invoices.invoice_date', '<=', $request->to_date);
|
||||
})
|
||||
->orderByDesc('containers.container_date')
|
||||
->orderByDesc('invoices.id')
|
||||
->get();
|
||||
|
||||
return view('admin.pdf.invoices_excel', compact('invoices'));
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,10 @@ namespace App\Http\Controllers\Admin;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Models\Container;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\User;
|
||||
use App\Models\MarkList;
|
||||
|
||||
class AdminAuthController extends Controller
|
||||
{
|
||||
@@ -31,16 +35,15 @@ class AdminAuthController extends Controller
|
||||
}
|
||||
|
||||
$credentials = [
|
||||
$field => $loginInput,
|
||||
$field => $loginInput,
|
||||
'password' => $request->password,
|
||||
];
|
||||
|
||||
// attempt login
|
||||
if (Auth::guard('admin')->attempt($credentials)) {
|
||||
$request->session()->regenerate();
|
||||
$user = Auth::guard('admin')->user();
|
||||
|
||||
return redirect()->route('admin.dashboard')->with('success', 'Welcome back, ' . $user->name . '!');
|
||||
return redirect()->route('admin.dashboard')
|
||||
->with('success', 'Welcome back, ' . $user->name . '!');
|
||||
}
|
||||
|
||||
return back()->withErrors(['login' => 'Invalid login credentials.']);
|
||||
@@ -51,6 +54,25 @@ class AdminAuthController extends Controller
|
||||
Auth::guard('admin')->logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
return redirect()->route('admin.login')->with('success', 'Logged out successfully.');
|
||||
return redirect()->route('admin.login')
|
||||
->with('success', 'Logged out successfully.');
|
||||
}
|
||||
}
|
||||
|
||||
public function profile()
|
||||
{
|
||||
$user = Auth::guard('admin')->user();
|
||||
|
||||
// ── Real Stats ──
|
||||
$stats = [
|
||||
'total_containers' => Container::count(),
|
||||
'total_invoices' => Invoice::count(),
|
||||
'paid_invoices' => Invoice::where('status', 'paid')->count(),
|
||||
'pending_invoices' => Invoice::where('status', 'pending')->count(),
|
||||
'total_customers' => User::count(),
|
||||
'total_marklist' => MarkList::count(),
|
||||
'active_marklist' => MarkList::where('status', 'active')->count(),
|
||||
];
|
||||
|
||||
return view('admin.profile', compact('user', 'stats'));
|
||||
}
|
||||
}
|
||||
@@ -19,32 +19,38 @@ class AdminCustomerController extends Controller
|
||||
$search = $request->search;
|
||||
$status = $request->status;
|
||||
|
||||
$query = User::with(['marks', 'orders'])->orderBy('id', 'desc');
|
||||
$query = User::with([
|
||||
'marks',
|
||||
'orders',
|
||||
'invoices.installments',
|
||||
'invoices.chargeGroups', // 🔥 for order total calculation
|
||||
])->orderBy('id', 'desc');
|
||||
|
||||
// SEARCH FILTER
|
||||
if (!empty($search)) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('customer_name', 'like', "%$search%")
|
||||
->orWhere('email', 'like', "%$search%")
|
||||
->orWhere('mobile_no', 'like', "%$search%")
|
||||
->orWhere('customer_id', 'like', "%$search%");
|
||||
->orWhere('email', 'like', "%$search%")
|
||||
->orWhere('mobile_no', 'like', "%$search%")
|
||||
->orWhere('customer_id', 'like', "%$search%");
|
||||
});
|
||||
}
|
||||
|
||||
// STATUS FILTER
|
||||
if (!empty($status) && in_array($status, ['active', 'inactive'])) {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
// Get all customers for statistics (without pagination)
|
||||
$allCustomers = $query->get();
|
||||
|
||||
// Get paginated customers for the table (10 per page)
|
||||
$customers = $query->paginate(10);
|
||||
|
||||
return view('admin.customers', compact('customers', 'allCustomers', 'search', 'status'));
|
||||
return view('admin.customers', compact(
|
||||
'customers',
|
||||
'allCustomers',
|
||||
'search',
|
||||
'status'
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// SHOW ADD CUSTOMER FORM
|
||||
// ---------------------------------------------------------
|
||||
@@ -106,20 +112,36 @@ class AdminCustomerController extends Controller
|
||||
// VIEW CUSTOMER FULL DETAILS
|
||||
// ---------------------------------------------------------
|
||||
public function view($id)
|
||||
{
|
||||
$customer = User::with(['marks', 'orders'])->findOrFail($id);
|
||||
{
|
||||
$customer = User::with([
|
||||
'marks',
|
||||
'orders',
|
||||
'invoices.installments'
|
||||
])->findOrFail($id);
|
||||
|
||||
$totalOrders = $customer->orders->count();
|
||||
$totalAmount = $customer->orders->sum('ttl_amount');
|
||||
$recentOrders = $customer->orders()->latest()->take(5)->get();
|
||||
// Orders
|
||||
$totalOrders = $customer->orders->count();
|
||||
$totalOrderAmount = $customer->orders->sum('ttl_amount');
|
||||
|
||||
// Invoices (PAYABLE)
|
||||
$totalPayable = $customer->invoices->sum('final_amount_with_gst');
|
||||
|
||||
// Paid via installments
|
||||
$totalPaid = $customer->invoiceInstallments->sum('amount');
|
||||
|
||||
// Remaining
|
||||
$totalRemaining = max($totalPayable - $totalPaid, 0);
|
||||
|
||||
return view('admin.customers_view', compact(
|
||||
'customer',
|
||||
'totalOrders',
|
||||
'totalOrderAmount',
|
||||
'totalPayable',
|
||||
'totalPaid',
|
||||
'totalRemaining'
|
||||
));
|
||||
}
|
||||
|
||||
return view('admin.customers_view', compact(
|
||||
'customer',
|
||||
'totalOrders',
|
||||
'totalAmount',
|
||||
'recentOrders'
|
||||
));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// TOGGLE STATUS ACTIVE / INACTIVE
|
||||
@@ -138,4 +160,4 @@ class AdminCustomerController extends Controller
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,39 +2,70 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller; // ⬅️ हे नक्की असू दे
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use Mpdf\Mpdf;
|
||||
use App\Models\InvoiceChargeGroup;
|
||||
use App\Models\InvoiceChargeGroupItem;
|
||||
use App\Models\InvoiceInstallment;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Mpdf\Mpdf;
|
||||
|
||||
|
||||
class AdminInvoiceController extends Controller
|
||||
{
|
||||
// -------------------------------------------------------------
|
||||
// INVOICE LIST PAGE
|
||||
// INDEX (LIST ALL INVOICES WITH FILTERS)
|
||||
// -------------------------------------------------------------
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
$invoices = Invoice::with(['order.shipments'])->latest()->get();
|
||||
$query = Invoice::query();
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('invoice_number', 'like', "%{$search}%")
|
||||
->orWhere('customer_name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('status') && $request->status !== 'all') {
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
if ($request->filled('start_date')) {
|
||||
$query->whereDate('invoice_date', '>=', $request->start_date);
|
||||
}
|
||||
|
||||
if ($request->filled('end_date')) {
|
||||
$query->whereDate('invoice_date', '<=', $request->end_date);
|
||||
}
|
||||
|
||||
$invoices = $query->latest()->get();
|
||||
|
||||
return view('admin.invoice', compact('invoices'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// POPUP VIEW (AJAX)
|
||||
// POPUP VIEW
|
||||
// -------------------------------------------------------------
|
||||
public function popup($id)
|
||||
{
|
||||
$invoice = Invoice::with(['items', 'order'])->findOrFail($id);
|
||||
$invoice = Invoice::with([
|
||||
'items',
|
||||
'chargeGroups.items',
|
||||
])->findOrFail($id);
|
||||
|
||||
// Find actual Shipment record
|
||||
$shipment = \App\Models\Shipment::whereHas('items', function ($q) use ($invoice) {
|
||||
$q->where('order_id', $invoice->order_id);
|
||||
})
|
||||
->first();
|
||||
$shipment = null;
|
||||
|
||||
return view('admin.popup_invoice', compact('invoice', 'shipment'));
|
||||
$groupedItemIds = $invoice->chargeGroups
|
||||
->flatMap(fn($group) => $group->items->pluck('invoice_item_id'))
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return view('admin.popup_invoice', compact('invoice', 'shipment', 'groupedItemIds'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
@@ -42,90 +73,135 @@ class AdminInvoiceController extends Controller
|
||||
// -------------------------------------------------------------
|
||||
public function edit($id)
|
||||
{
|
||||
$invoice = Invoice::with(['order.shipments'])->findOrFail($id);
|
||||
$shipment = $invoice->order?->shipments?->first();
|
||||
$invoice = Invoice::with([
|
||||
'items',
|
||||
'customer',
|
||||
'container',
|
||||
'chargeGroups.items',
|
||||
'installments',
|
||||
])->findOrFail($id);
|
||||
|
||||
return view('admin.invoice_edit', compact('invoice', 'shipment'));
|
||||
// ✅ Customer details sync
|
||||
if ($invoice->customer) {
|
||||
$needsUpdate = [];
|
||||
|
||||
if (empty($invoice->customer_email) || $invoice->customer_email === 'test@demo.com') {
|
||||
$needsUpdate['customer_email'] = $invoice->customer->email;
|
||||
}
|
||||
if (empty($invoice->customer_address) || $invoice->customer_address === 'TEST ADDRESS') {
|
||||
$needsUpdate['customer_address'] = $invoice->customer->address;
|
||||
}
|
||||
if (empty($invoice->pincode) || $invoice->pincode === '999999') {
|
||||
$needsUpdate['pincode'] = $invoice->customer->pincode;
|
||||
}
|
||||
|
||||
if (!empty($needsUpdate)) {
|
||||
$invoice->update($needsUpdate);
|
||||
$invoice->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
$shipment = null;
|
||||
|
||||
$groupedItemIds = $invoice->chargeGroups
|
||||
->flatMap(function ($group) {
|
||||
return $group->items->pluck('invoice_item_id');
|
||||
})
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return view('admin.invoice_edit', compact('invoice', 'shipment', 'groupedItemIds'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// UPDATE INVOICE
|
||||
// UPDATE INVOICE (HEADER ONLY)
|
||||
// -------------------------------------------------------------
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
Log::info("🟡 Invoice Update Request Received", [
|
||||
Log::info('🟡 Invoice Update Request Received', [
|
||||
'invoice_id' => $id,
|
||||
'request' => $request->all()
|
||||
'request' => $request->all(),
|
||||
]);
|
||||
|
||||
$invoice = Invoice::findOrFail($id);
|
||||
|
||||
$data = $request->validate([
|
||||
'invoice_date' => 'required|date',
|
||||
'due_date' => 'required|date|after_or_equal:invoice_date',
|
||||
'final_amount' => 'required|numeric|min:0',
|
||||
'tax_type' => 'required|in:gst,igst',
|
||||
'tax_percent' => 'required|numeric|min:0|max:28',
|
||||
'status' => 'required|in:pending,paid,overdue',
|
||||
'notes' => 'nullable|string',
|
||||
'invoice_date' => 'required|date',
|
||||
'due_date' => 'required|date|after_or_equal:invoice_date',
|
||||
'status' => 'required|in:pending,paying,paid,overdue',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
|
||||
Log::info("✅ Validated Invoice Update Data", $data);
|
||||
|
||||
$finalAmount = floatval($data['final_amount']);
|
||||
$taxPercent = floatval($data['tax_percent']);
|
||||
$taxAmount = 0;
|
||||
|
||||
if ($data['tax_type'] === 'gst') {
|
||||
Log::info("🟢 GST Selected", compact('taxPercent'));
|
||||
$data['cgst_percent'] = $taxPercent / 2;
|
||||
$data['sgst_percent'] = $taxPercent / 2;
|
||||
$data['igst_percent'] = 0;
|
||||
} else {
|
||||
Log::info("🔵 IGST Selected", compact('taxPercent'));
|
||||
$data['cgst_percent'] = 0;
|
||||
$data['sgst_percent'] = 0;
|
||||
$data['igst_percent'] = $taxPercent;
|
||||
}
|
||||
|
||||
$taxAmount = ($finalAmount * $taxPercent) / 100;
|
||||
|
||||
$data['gst_amount'] = $taxAmount;
|
||||
$data['final_amount_with_gst'] = $finalAmount + $taxAmount;
|
||||
$data['gst_percent'] = $taxPercent;
|
||||
|
||||
Log::info("📌 Final Calculated Invoice Values", [
|
||||
'invoice_id' => $invoice->id,
|
||||
'final_amount' => $finalAmount,
|
||||
'gst_amount' => $data['gst_amount'],
|
||||
'final_amount_with_gst' => $data['final_amount_with_gst'],
|
||||
'tax_type' => $data['tax_type'],
|
||||
'cgst_percent' => $data['cgst_percent'],
|
||||
'sgst_percent' => $data['sgst_percent'],
|
||||
'igst_percent' => $data['igst_percent'],
|
||||
]);
|
||||
Log::info('✅ Validated Invoice Header Update Data', $data);
|
||||
|
||||
$invoice->update($data);
|
||||
$invoice->refresh();
|
||||
|
||||
Log::info("✅ Invoice Updated Successfully", [
|
||||
'invoice_id' => $invoice->id
|
||||
Log::info('🔍 Invoice AFTER HEADER UPDATE', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'charge_groups_total' => $invoice->charge_groups_total,
|
||||
'gst_amount' => $invoice->gst_amount,
|
||||
'grand_total_with_charges'=> $invoice->grand_total_with_charges,
|
||||
]);
|
||||
|
||||
// regenerate PDF
|
||||
$this->generateInvoicePDF($invoice);
|
||||
|
||||
return redirect()
|
||||
->route('admin.invoices.index')
|
||||
->route('admin.invoices.edit', $invoice->id)
|
||||
->with('success', 'Invoice updated & PDF generated successfully.');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PDF GENERATION USING mPDF
|
||||
// UPDATE INVOICE ITEMS (फक्त items save)
|
||||
// -------------------------------------------------------------
|
||||
public function updateItems(Request $request, Invoice $invoice)
|
||||
{
|
||||
Log::info('🟡 Invoice Items Update Request', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'payload' => $request->all(),
|
||||
]);
|
||||
|
||||
$data = $request->validate([
|
||||
'items' => ['required', 'array'],
|
||||
'items.*.price' => ['required', 'numeric', 'min:0'],
|
||||
'items.*.ttl_amount' => ['required', 'numeric', 'min:0'],
|
||||
]);
|
||||
|
||||
foreach ($data['items'] as $itemId => $itemData) {
|
||||
$item = InvoiceItem::where('id', $itemId)
|
||||
->where('invoice_id', $invoice->id)
|
||||
->first();
|
||||
|
||||
if (!$item) {
|
||||
Log::warning('Invoice item not found or mismatched invoice', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'item_id' => $itemId,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$item->price = $itemData['price'];
|
||||
$item->ttl_amount = $itemData['ttl_amount'];
|
||||
$item->save();
|
||||
}
|
||||
|
||||
Log::info('✅ Invoice items updated (no totals recalculation)', [
|
||||
'invoice_id' => $invoice->id,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Invoice items updated successfully.');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PDF GENERATION (EXISTING - केवळ chargeGroups load ला confirm कर)
|
||||
// -------------------------------------------------------------
|
||||
public function generateInvoicePDF($invoice)
|
||||
{
|
||||
$invoice->load(['items', 'order.shipments']);
|
||||
$shipment = $invoice->order?->shipments?->first();
|
||||
// ✅ यामध्ये chargeGroups आणि installments load कर
|
||||
$invoice->load(['items', 'customer', 'container', 'chargeGroups.items', 'installments']);
|
||||
$shipment = null;
|
||||
|
||||
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
|
||||
$folder = public_path('invoices/');
|
||||
|
||||
@@ -134,98 +210,295 @@ class AdminInvoiceController extends Controller
|
||||
}
|
||||
|
||||
$filePath = $folder . $fileName;
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
$mpdf = new Mpdf(['mode' => 'utf-8', 'format' => 'A4', 'default_font' => 'sans-serif']);
|
||||
$html = view('admin.pdf.invoice', ['invoice' => $invoice, 'shipment' => $shipment])->render();
|
||||
$mpdf = new Mpdf([
|
||||
'mode' => 'utf-8',
|
||||
'format' => 'A4',
|
||||
'default_font' => 'sans-serif',
|
||||
]);
|
||||
|
||||
$html = view('admin.pdf.invoice', [
|
||||
'invoice' => $invoice,
|
||||
'shipment' => $shipment,
|
||||
])->render();
|
||||
|
||||
$mpdf->WriteHTML($html);
|
||||
$mpdf->Output($filePath, 'F');
|
||||
|
||||
$invoice->update(['pdf_path' => 'invoices/' . $fileName]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INSTALLMENTS (ADD/DELETE)
|
||||
// INSTALLMENTS (ADD)
|
||||
// -------------------------------------------------------------
|
||||
public function storeInstallment(Request $request, $invoice_id)
|
||||
{
|
||||
$request->validate([
|
||||
'installment_date' => 'required|date',
|
||||
'payment_method' => 'required|string',
|
||||
'reference_no' => 'nullable|string',
|
||||
'amount' => 'required|numeric|min:1',
|
||||
'payment_method' => 'required|string',
|
||||
'reference_no' => 'nullable|string',
|
||||
'amount' => 'required|numeric|min:1',
|
||||
]);
|
||||
|
||||
$invoice = Invoice::findOrFail($invoice_id);
|
||||
|
||||
$grandTotal = $invoice->grand_total_with_charges ?? 0;
|
||||
|
||||
$paidTotal = $invoice->installments()->sum('amount');
|
||||
// Use GST-inclusive total for all calculations/checks
|
||||
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
||||
$remaining = $grandTotal - $paidTotal;
|
||||
|
||||
if ($request->amount > $remaining) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Installment amount exceeds remaining balance.'
|
||||
'message' => 'Installment amount exceeds remaining balance.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$installment = InvoiceInstallment::create([
|
||||
'invoice_id' => $invoice_id,
|
||||
'invoice_id' => $invoice_id,
|
||||
'installment_date' => $request->installment_date,
|
||||
'payment_method' => $request->payment_method,
|
||||
'reference_no' => $request->reference_no,
|
||||
'amount' => $request->amount,
|
||||
'payment_method' => $request->payment_method,
|
||||
'reference_no' => $request->reference_no,
|
||||
'amount' => $request->amount,
|
||||
]);
|
||||
|
||||
$newPaid = $paidTotal + $request->amount;
|
||||
$remaining = max(0, $grandTotal - $newPaid);
|
||||
|
||||
// Mark as 'paid' if GST-inclusive total is cleared
|
||||
if ($newPaid >= $invoice->final_amount_with_gst) {
|
||||
$invoice->update(['status' => 'paid']);
|
||||
$isOverdue = now()->startOfDay()->gt(\Carbon\Carbon::parse($invoice->due_date)->startOfDay());
|
||||
|
||||
$this->generateInvoicePDF($invoice);
|
||||
if ($grandTotal > 0 && $newPaid >= $grandTotal) {
|
||||
$newStatus = 'paid';
|
||||
} elseif ($newPaid > 0 && $isOverdue) {
|
||||
$newStatus = 'overdue';
|
||||
} elseif ($newPaid > 0 && !$isOverdue) {
|
||||
$newStatus = 'paying';
|
||||
} elseif ($newPaid <= 0 && $isOverdue) {
|
||||
$newStatus = 'overdue';
|
||||
} else {
|
||||
$newStatus = 'pending';
|
||||
}
|
||||
|
||||
$invoice->update([
|
||||
'payment_method' => $request->payment_method,
|
||||
'reference_no' => $request->reference_no,
|
||||
'status' => $newStatus,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Installment added successfully.',
|
||||
'installment' => $installment,
|
||||
'totalPaid' => $newPaid,
|
||||
'gstAmount' => $invoice->gst_amount,
|
||||
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
||||
'baseAmount' => $invoice->final_amount,
|
||||
'remaining' => max(0, $invoice->final_amount_with_gst - $newPaid),
|
||||
'isCompleted' => $newPaid >= $invoice->final_amount_with_gst
|
||||
'status' => 'success',
|
||||
'message' => 'Installment added successfully.',
|
||||
'installment' => $installment,
|
||||
'chargeGroupsTotal' => $invoice->charge_groups_total ?? 0,
|
||||
'gstAmount' => $invoice->gst_amount ?? 0,
|
||||
'grandTotal' => $grandTotal,
|
||||
'totalPaid' => $newPaid,
|
||||
'remaining' => $remaining,
|
||||
'newStatus' => $newStatus,
|
||||
'isCompleted' => $remaining <= 0,
|
||||
'isZero' => $newPaid == 0,
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INSTALLMENTS (DELETE)
|
||||
// -------------------------------------------------------------
|
||||
public function deleteInstallment($id)
|
||||
{
|
||||
$installment = InvoiceInstallment::findOrFail($id);
|
||||
$invoice = $installment->invoice;
|
||||
|
||||
$installment->delete();
|
||||
|
||||
$paidTotal = $invoice->installments()->sum('amount');
|
||||
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
||||
|
||||
// Update status if not fully paid anymore
|
||||
if ($remaining > 0 && $invoice->status === "paid") {
|
||||
$invoice->update(['status' => 'pending']);
|
||||
|
||||
$this->generateInvoicePDF($invoice);
|
||||
$installment->delete();
|
||||
$invoice->refresh();
|
||||
|
||||
$grandTotal = $invoice->grand_total_with_charges ?? 0;
|
||||
|
||||
$paidTotal = $invoice->installments()->sum('amount');
|
||||
$remaining = max(0, $grandTotal - $paidTotal);
|
||||
|
||||
$isOverdue = now()->startOfDay()->gt(\Carbon\Carbon::parse($invoice->due_date)->startOfDay());
|
||||
|
||||
if ($grandTotal > 0 && $paidTotal >= $grandTotal) {
|
||||
$newStatus = 'paid';
|
||||
} elseif ($paidTotal > 0 && $isOverdue) {
|
||||
$newStatus = 'overdue';
|
||||
} elseif ($paidTotal > 0 && !$isOverdue) {
|
||||
$newStatus = 'paying';
|
||||
} elseif ($paidTotal <= 0 && $isOverdue) {
|
||||
$newStatus = 'overdue';
|
||||
} else {
|
||||
$newStatus = 'pending';
|
||||
}
|
||||
|
||||
|
||||
$invoice->update(['status' => $newStatus]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Installment deleted.',
|
||||
'totalPaid' => $paidTotal,
|
||||
'gstAmount' => $invoice->gst_amount,
|
||||
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
||||
'baseAmount' => $invoice->final_amount,
|
||||
'remaining' => $remaining,
|
||||
'isZero' => $paidTotal == 0
|
||||
'status' => 'success',
|
||||
'message' => 'Installment deleted.',
|
||||
'chargeGroupsTotal' => $invoice->charge_groups_total ?? 0,
|
||||
'gstAmount' => $invoice->gst_amount ?? 0,
|
||||
'grandTotal' => $grandTotal,
|
||||
'totalPaid' => $paidTotal,
|
||||
'remaining' => $remaining,
|
||||
'newStatus' => $newStatus,
|
||||
'isZero' => $paidTotal == 0,
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// CHARGE GROUP SAVE
|
||||
// -------------------------------------------------------------
|
||||
public function storeChargeGroup(Request $request, $invoiceId)
|
||||
{
|
||||
Log::info('🟡 storeChargeGroup HIT', [
|
||||
'invoice_id' => $invoiceId,
|
||||
'payload' => $request->all(),
|
||||
]);
|
||||
|
||||
$invoice = Invoice::with('items', 'chargeGroups')->findOrFail($invoiceId);
|
||||
|
||||
$data = $request->validate([
|
||||
'groupname' => 'required|string|max:255',
|
||||
'basistype' => 'required|in:ttl_qty,amount,ttl_cbm,ttl_kg',
|
||||
'basisvalue' => 'required|numeric',
|
||||
'rate' => 'required|numeric|min:0.0001',
|
||||
'autototal' => 'required|numeric|min:0.01',
|
||||
'itemids' => 'required|array',
|
||||
'itemids.*' => 'integer|exists:invoice_items,id',
|
||||
|
||||
'tax_type' => 'nullable|in:none,gst,igst',
|
||||
'gst_percent' => 'nullable|numeric|min:0|max:28',
|
||||
'total_with_gst' => 'nullable|numeric|min:0',
|
||||
]);
|
||||
|
||||
Log::info('✅ storeChargeGroup VALIDATED', $data);
|
||||
|
||||
$exists = InvoiceChargeGroup::where('invoice_id', $invoice->id)
|
||||
->where('group_name', $data['groupname'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return back()
|
||||
->withErrors(['groupname' => 'This group name is already used for this invoice.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
$taxType = $data['tax_type'] ?? 'gst';
|
||||
$gstPercent = $data['gst_percent'] ?? 0;
|
||||
$baseTotal = $data['autototal'];
|
||||
|
||||
$totalWithGst = $data['total_with_gst'] ?? $baseTotal;
|
||||
if ($totalWithGst == 0 && $gstPercent > 0) {
|
||||
$gstAmount = ($baseTotal * $gstPercent) / 100;
|
||||
$totalWithGst = $baseTotal + $gstAmount;
|
||||
}
|
||||
|
||||
$group = InvoiceChargeGroup::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
'group_name' => $data['groupname'],
|
||||
'basis_type' => $data['basistype'],
|
||||
'basis_value' => $data['basisvalue'],
|
||||
'rate' => $data['rate'],
|
||||
'total_charge' => $baseTotal,
|
||||
'tax_type' => $taxType,
|
||||
'gst_percent' => $gstPercent,
|
||||
'total_with_gst' => $totalWithGst,
|
||||
]);
|
||||
|
||||
foreach ($data['itemids'] as $itemId) {
|
||||
InvoiceChargeGroupItem::create([
|
||||
'group_id' => $group->id,
|
||||
'invoice_item_id' => $itemId,
|
||||
]);
|
||||
}
|
||||
|
||||
$invoice->load('chargeGroups');
|
||||
|
||||
$chargeGroupsBase = $invoice->chargeGroups->sum('total_charge');
|
||||
$chargeGroupsWithG = $invoice->chargeGroups->sum('total_with_gst');
|
||||
$chargeGroupsGst = $chargeGroupsWithG - $chargeGroupsBase;
|
||||
|
||||
$invoiceGstPercent = $group->gst_percent ?? 0;
|
||||
$invoiceTaxType = $group->tax_type ?? 'gst';
|
||||
|
||||
$cgstPercent = 0;
|
||||
$sgstPercent = 0;
|
||||
$igstPercent = 0;
|
||||
|
||||
if ($invoiceTaxType === 'gst') {
|
||||
$cgstPercent = $invoiceGstPercent / 2;
|
||||
$sgstPercent = $invoiceGstPercent / 2;
|
||||
} elseif ($invoiceTaxType === 'igst') {
|
||||
$igstPercent = $invoiceGstPercent;
|
||||
}
|
||||
|
||||
$invoice->update([
|
||||
'charge_groups_total' => $chargeGroupsBase,
|
||||
'gst_amount' => $chargeGroupsGst,
|
||||
'gst_percent' => $invoiceGstPercent,
|
||||
'tax_type' => $invoiceTaxType,
|
||||
'cgst_percent' => $cgstPercent,
|
||||
'sgst_percent' => $sgstPercent,
|
||||
'igst_percent' => $igstPercent,
|
||||
|
||||
'final_amount' => $chargeGroupsBase,
|
||||
'final_amount_with_gst' => $chargeGroupsWithG,
|
||||
'grand_total_with_charges' => $chargeGroupsWithG,
|
||||
]);
|
||||
|
||||
Log::info('✅ Invoice updated from Charge Group (CG total + GST)', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'charge_groups_total' => $chargeGroupsBase,
|
||||
'gst_amount' => $chargeGroupsGst,
|
||||
'grand_total_with_charges'=> $invoice->grand_total_with_charges,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Charge group saved successfully.',
|
||||
'group_id' => $group->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 🆕 PDF DOWNLOAD (Direct browser download)
|
||||
// ============================================
|
||||
public function downloadPdf($id)
|
||||
{
|
||||
$invoice = Invoice::with(['items', 'customer', 'container', 'chargeGroups.items', 'installments'])
|
||||
->findOrFail($id);
|
||||
|
||||
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
|
||||
$folder = public_path('invoices/');
|
||||
$filePath = $folder . $fileName;
|
||||
|
||||
// जर PDF exist नसेल तर generate कर
|
||||
if (!file_exists($filePath)) {
|
||||
$this->generateInvoicePDF($invoice);
|
||||
}
|
||||
|
||||
return response()->download($filePath, $fileName);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 🆕 EXCEL DOWNLOAD (CSV format - simple)
|
||||
// ============================================
|
||||
|
||||
|
||||
public function share($id)
|
||||
{
|
||||
$invoice = Invoice::findOrFail($id);
|
||||
|
||||
// इथे तुला जसं share करायचंय तसं logic टाक:
|
||||
// उदा. public link generate करून redirect कर, किंवा WhatsApp deeplink, इ.
|
||||
|
||||
$url = route('admin.invoices.popup', $invoice->id); // example: popup link share
|
||||
|
||||
return redirect()->away('https://wa.me/?text=' . urlencode($url));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,54 +3,199 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Order;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Mpdf\Mpdf;
|
||||
|
||||
class AdminReportController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the reports page with joined data
|
||||
*/
|
||||
public function index(Request $request)
|
||||
// UI साठी main action
|
||||
public function containerReport(Request $request)
|
||||
{
|
||||
// -------------------------------
|
||||
// FETCH REPORT DATA
|
||||
// ONLY orders that have BOTH:
|
||||
// 1. Invoice
|
||||
// 2. Shipment
|
||||
// -------------------------------
|
||||
$reports = DB::table('orders')
|
||||
->join('shipment_items', 'shipment_items.order_id', '=', 'orders.id')
|
||||
->join('shipments', 'shipments.id', '=', 'shipment_items.shipment_id')
|
||||
->join('invoices', 'invoices.order_id', '=', 'orders.id')
|
||||
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'orders.mark_no')
|
||||
->leftJoin('users', 'users.customer_id', '=', 'mark_list.customer_id')
|
||||
|
||||
->select(
|
||||
'orders.id as order_pk',
|
||||
'orders.order_id',
|
||||
'orders.mark_no',
|
||||
'orders.origin',
|
||||
'orders.destination',
|
||||
|
||||
'shipments.id as shipment_pk',
|
||||
'shipments.shipment_id',
|
||||
'shipments.status as shipment_status',
|
||||
'shipments.shipment_date',
|
||||
|
||||
'invoices.invoice_number',
|
||||
'invoices.invoice_date',
|
||||
'invoices.final_amount',
|
||||
'invoices.status as invoice_status',
|
||||
|
||||
'mark_list.company_name',
|
||||
'mark_list.customer_name'
|
||||
)
|
||||
|
||||
->orderBy('shipments.shipment_date', 'desc')
|
||||
->get();
|
||||
$reports = $this->buildContainerReportQuery($request)->get();
|
||||
|
||||
return view('admin.reports', compact('reports'));
|
||||
}
|
||||
}
|
||||
|
||||
// ही common query — filters accept करते
|
||||
protected function buildContainerReportQuery(Request $request = null)
|
||||
{
|
||||
$query = DB::table('invoices')
|
||||
->join('containers', 'containers.id', '=', 'invoices.container_id')
|
||||
->leftJoinSub(
|
||||
DB::table('invoice_installments')
|
||||
->select('invoice_id', DB::raw('COALESCE(SUM(amount), 0) as total_paid'))
|
||||
->groupBy('invoice_id'),
|
||||
'inst',
|
||||
'inst.invoice_id',
|
||||
'=',
|
||||
'invoices.id'
|
||||
)
|
||||
->select(
|
||||
'containers.id as container_id',
|
||||
'containers.container_number',
|
||||
'containers.container_date',
|
||||
'containers.container_name',
|
||||
|
||||
DB::raw('COUNT(DISTINCT invoices.mark_no) as total_mark_nos'),
|
||||
DB::raw('COUNT(DISTINCT invoices.customer_id) as total_customers'),
|
||||
DB::raw('COUNT(invoices.id) as total_invoices'),
|
||||
|
||||
DB::raw('COALESCE(SUM(invoices.charge_groups_total), 0) as total_invoice_amount'),
|
||||
DB::raw('COALESCE(SUM(invoices.gst_amount), 0) as total_gst_amount'),
|
||||
DB::raw('COALESCE(SUM(invoices.grand_total_with_charges), 0) as total_payable'),
|
||||
DB::raw('COALESCE(SUM(inst.total_paid), 0) as total_paid'),
|
||||
DB::raw('GREATEST(0, COALESCE(SUM(invoices.grand_total_with_charges), 0) - COALESCE(SUM(inst.total_paid), 0)) as total_remaining'),
|
||||
|
||||
DB::raw("
|
||||
CASE
|
||||
WHEN COUNT(invoices.id) > 0
|
||||
AND SUM(CASE WHEN invoices.status != 'paid' THEN 1 ELSE 0 END) = 0
|
||||
THEN 'paid'
|
||||
WHEN SUM(CASE WHEN invoices.status = 'overdue' THEN 1 ELSE 0 END) > 0
|
||||
THEN 'overdue'
|
||||
WHEN SUM(CASE WHEN invoices.status = 'paying' THEN 1 ELSE 0 END) > 0
|
||||
THEN 'paying'
|
||||
ELSE 'pending'
|
||||
END as container_status
|
||||
")
|
||||
)
|
||||
->groupBy(
|
||||
'containers.id',
|
||||
'containers.container_number',
|
||||
'containers.container_date',
|
||||
'containers.container_name'
|
||||
)
|
||||
->orderBy('containers.container_date', 'desc')
|
||||
->orderBy('containers.id', 'desc');
|
||||
|
||||
// ── Filters ──────────────────────────────────────────────────────
|
||||
if ($request) {
|
||||
if ($request->filled('from_date')) {
|
||||
$query->whereDate('containers.container_date', '>=', $request->from_date);
|
||||
}
|
||||
|
||||
if ($request->filled('to_date')) {
|
||||
$query->whereDate('containers.container_date', '<=', $request->to_date);
|
||||
}
|
||||
|
||||
if ($request->filled('status')) {
|
||||
// container_status हे aggregate expression आहे,
|
||||
// त्यामुळे HAVING clause वापरतो
|
||||
$query->havingRaw("
|
||||
CASE
|
||||
WHEN COUNT(invoices.id) > 0
|
||||
AND SUM(CASE WHEN invoices.status != 'paid' THEN 1 ELSE 0 END) = 0
|
||||
THEN 'paid'
|
||||
WHEN SUM(CASE WHEN invoices.status = 'overdue' THEN 1 ELSE 0 END) > 0
|
||||
THEN 'overdue'
|
||||
WHEN SUM(CASE WHEN invoices.status = 'paying' THEN 1 ELSE 0 END) > 0
|
||||
THEN 'paying'
|
||||
ELSE 'pending'
|
||||
END = ?
|
||||
", [$request->status]);
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
// ---- Excel export ----
|
||||
public function containerReportExcel(Request $request)
|
||||
{
|
||||
$reports = $this->buildContainerReportQuery($request)->get();
|
||||
|
||||
$headings = [
|
||||
'Container No',
|
||||
'Container Date',
|
||||
'Total Mark Nos',
|
||||
'Total Customers',
|
||||
'Total Invoices',
|
||||
'Invoice Amount (Before GST)',
|
||||
'GST Amount',
|
||||
'Payable Amount (Incl. GST)',
|
||||
'Paid Amount',
|
||||
'Remaining Amount',
|
||||
'Container Status',
|
||||
];
|
||||
|
||||
$rows = $reports->map(function ($r) {
|
||||
return [
|
||||
$r->container_number,
|
||||
$r->container_date,
|
||||
$r->total_mark_nos,
|
||||
$r->total_customers,
|
||||
$r->total_invoices,
|
||||
$r->total_invoice_amount,
|
||||
$r->total_gst_amount,
|
||||
$r->total_payable,
|
||||
$r->total_paid,
|
||||
$r->total_remaining,
|
||||
$r->container_status,
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
$export = new class($headings, $rows) implements
|
||||
\Maatwebsite\Excel\Concerns\FromArray,
|
||||
\Maatwebsite\Excel\Concerns\WithHeadings
|
||||
{
|
||||
private $headings;
|
||||
private $rows;
|
||||
|
||||
public function __construct($headings, $rows)
|
||||
{
|
||||
$this->headings = $headings;
|
||||
$this->rows = $rows;
|
||||
}
|
||||
|
||||
public function array(): array
|
||||
{
|
||||
return $this->rows;
|
||||
}
|
||||
|
||||
public function headings(): array
|
||||
{
|
||||
return $this->headings;
|
||||
}
|
||||
};
|
||||
|
||||
return Excel::download(
|
||||
$export,
|
||||
'container-report-' . now()->format('Ymd-His') . '.xlsx'
|
||||
);
|
||||
}
|
||||
|
||||
// ---- PDF export ----
|
||||
public function containerReportPdf(Request $request)
|
||||
{
|
||||
$reports = $this->buildContainerReportQuery($request)->get();
|
||||
|
||||
$html = view('admin.reports', [
|
||||
'reports' => $reports,
|
||||
'isPdf' => true,
|
||||
])->render();
|
||||
|
||||
$mpdf = new \Mpdf\Mpdf([
|
||||
'mode' => 'utf-8',
|
||||
'format' => 'A4-L',
|
||||
'default_font' => 'dejavusans',
|
||||
'margin_top' => 8,
|
||||
'margin_right' => 8,
|
||||
'margin_bottom' => 10,
|
||||
'margin_left' => 8,
|
||||
]);
|
||||
|
||||
$mpdf->SetHTMLHeader('');
|
||||
$mpdf->SetHTMLFooter('');
|
||||
|
||||
$mpdf->WriteHTML($html);
|
||||
|
||||
$fileName = 'container-report-' . now()->format('Ymd-His') . '.pdf';
|
||||
|
||||
return response($mpdf->Output($fileName, 'S'), 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ class AdminStaffController extends Controller
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
// 1️⃣ Create staff WITHOUT employee_id (ID not available yet)
|
||||
$admin = Admin::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
@@ -69,23 +70,33 @@ class AdminStaffController extends Controller
|
||||
'status' => $request->status,
|
||||
'additional_info' => $request->additional_info,
|
||||
|
||||
'username' => $request->username,
|
||||
// username may be NULL here
|
||||
'username' => $request->username ?: null,
|
||||
'password' => Hash::make($request->password),
|
||||
'type' => 'staff',
|
||||
]);
|
||||
|
||||
// Generate EMPLOYEE ID using admin ID (safe)
|
||||
// 2️⃣ Generate EMPLOYEE ID
|
||||
$employeeId = 'EMP' . str_pad($admin->id, 4, '0', STR_PAD_LEFT);
|
||||
$admin->update(['employee_id' => $employeeId]);
|
||||
|
||||
// Assign permissions (if any)
|
||||
// 3️⃣ Auto-generate username if left blank
|
||||
$username = $request->username ?: strtolower($employeeId);
|
||||
|
||||
// 4️⃣ Update employee_id + username together
|
||||
$admin->update([
|
||||
'employee_id' => $employeeId,
|
||||
'username' => $username,
|
||||
]);
|
||||
|
||||
// 5️⃣ Assign permissions (if any)
|
||||
if ($request->permissions) {
|
||||
$admin->givePermissionTo($request->permissions);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
return redirect()->route('admin.staff.index')
|
||||
return redirect()
|
||||
->route('admin.staff.index')
|
||||
->with('success', 'Staff created successfully.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
@@ -94,6 +105,7 @@ class AdminStaffController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$staff = Admin::where('type', 'staff')->findOrFail($id);
|
||||
|
||||
@@ -20,7 +20,11 @@ class ShipmentController extends Controller
|
||||
$usedOrderIds = ShipmentItem::pluck('order_id')->toArray();
|
||||
|
||||
// 2) Load available orders (not used in any shipment)
|
||||
$availableOrders = Order::whereNotIn('id', $usedOrderIds)->get();
|
||||
$availableOrders = Order::whereNotIn('id', $usedOrderIds)
|
||||
->where('status', '!=', 'order_placed')
|
||||
->get();
|
||||
|
||||
|
||||
|
||||
// 3) Load all shipments for listing
|
||||
$shipments = Shipment::latest()->get();
|
||||
@@ -65,6 +69,16 @@ class ShipmentController extends Controller
|
||||
// CALCULATE TOTALS
|
||||
// -----------------------------
|
||||
$orders = Order::whereIn('id', $request->order_ids)->get();
|
||||
foreach ($orders as $order) {
|
||||
if ($order->status === 'order_placed') {
|
||||
return back()->with(
|
||||
'error',
|
||||
"Order {$order->order_id} is not ready for shipment"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
$total_ctn = $orders->sum('ctn');
|
||||
$total_qty = $orders->sum('qty');
|
||||
@@ -82,7 +96,7 @@ class ShipmentController extends Controller
|
||||
'shipment_id' => $newShipmentId,
|
||||
'origin' => $request->origin,
|
||||
'destination' => $request->destination,
|
||||
'status' => Shipment::STATUS_PENDING,
|
||||
'status' => Shipment::STATUS_SHIPMENT_READY,
|
||||
'shipment_date' => $request->shipment_date,
|
||||
|
||||
'total_ctn' => $total_ctn,
|
||||
@@ -135,29 +149,35 @@ class ShipmentController extends Controller
|
||||
* Update Shipment status from action button
|
||||
*/
|
||||
public function updateStatus(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'shipment_id' => 'required|exists:shipments,id',
|
||||
'status' => 'required|string'
|
||||
]);
|
||||
{
|
||||
$request->validate([
|
||||
'shipment_id' => 'required|exists:shipments,id',
|
||||
'status' => 'required|string'
|
||||
]);
|
||||
|
||||
// 1) Update shipment status
|
||||
$shipment = Shipment::findOrFail($request->shipment_id);
|
||||
$shipment->status = $request->status;
|
||||
$shipment->save();
|
||||
$shipment = Shipment::findOrFail($request->shipment_id);
|
||||
$shipment->status = $request->status;
|
||||
$shipment->save();
|
||||
|
||||
// 2) Update ALL related orders' status
|
||||
foreach ($shipment->orders as $order) {
|
||||
$order->status = $shipment->status; // status is string: pending, in_transit, dispatched, delivered
|
||||
$order->save();
|
||||
// ✅ Sync shipment status to orders ONLY after shipment exists
|
||||
foreach ($shipment->orders as $order) {
|
||||
|
||||
// Prevent rollback or overwrite
|
||||
if ($order->status === 'delivered') {
|
||||
continue;
|
||||
}
|
||||
|
||||
return redirect()->back()->with(
|
||||
'success',
|
||||
"Shipment status updated to {$shipment->statusLabel()} and related orders updated."
|
||||
);
|
||||
$order->status = $shipment->status;
|
||||
$order->save();
|
||||
}
|
||||
|
||||
return redirect()->back()->with(
|
||||
'success',
|
||||
"Shipment status updated to {$shipment->statusLabel()}."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update shipment details
|
||||
*/
|
||||
@@ -266,11 +286,23 @@ public function addOrders(Request $request, Shipment $shipment)
|
||||
'order_ids' => 'required|array|min:1',
|
||||
]);
|
||||
|
||||
// फक्त न वापरलेले orders घ्या
|
||||
$orders = Order::whereIn('id', $request->order_ids)->get();
|
||||
|
||||
foreach ($orders as $order) {
|
||||
// pivot मध्ये insert
|
||||
|
||||
if ($order->status === 'order_placed') {
|
||||
return back()->with(
|
||||
'error',
|
||||
"Order {$order->order_id} is not ready for shipment"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Prevent duplicates
|
||||
if (ShipmentItem::where('order_id', $order->id)->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ShipmentItem::create([
|
||||
'shipment_id' => $shipment->id,
|
||||
'order_id' => $order->id,
|
||||
@@ -282,23 +314,25 @@ public function addOrders(Request $request, Shipment $shipment)
|
||||
]);
|
||||
}
|
||||
|
||||
// totals
|
||||
// Recalculate totals
|
||||
$orderIds = ShipmentItem::where('shipment_id', $shipment->id)->pluck('order_id');
|
||||
$allOrders = Order::whereIn('id', $orderIds)->get();
|
||||
|
||||
$shipment->total_ctn = $allOrders->sum('ctn');
|
||||
$shipment->total_qty = $allOrders->sum('qty');
|
||||
$shipment->total_ttl_qty = $allOrders->sum('ttl_qty');
|
||||
$shipment->total_cbm = $allOrders->sum('cbm');
|
||||
$shipment->total_ttl_cbm = $allOrders->sum('ttl_cbm');
|
||||
$shipment->total_kg = $allOrders->sum('kg');
|
||||
$shipment->total_ttl_kg = $allOrders->sum('ttl_kg');
|
||||
$shipment->total_amount = $allOrders->sum('ttl_amount');
|
||||
$shipment->save();
|
||||
$shipment->update([
|
||||
'total_ctn' => $allOrders->sum('ctn'),
|
||||
'total_qty' => $allOrders->sum('qty'),
|
||||
'total_ttl_qty' => $allOrders->sum('ttl_qty'),
|
||||
'total_cbm' => $allOrders->sum('cbm'),
|
||||
'total_ttl_cbm' => $allOrders->sum('ttl_cbm'),
|
||||
'total_kg' => $allOrders->sum('kg'),
|
||||
'total_ttl_kg' => $allOrders->sum('ttl_kg'),
|
||||
'total_amount' => $allOrders->sum('ttl_amount'),
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.shipments.dummy', $shipment->id)
|
||||
->with('success', 'Orders added to shipment successfully.');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -15,7 +15,12 @@ class UserRequestController extends Controller
|
||||
public function index()
|
||||
{
|
||||
$requests = CustomerRequest::orderBy('id', 'desc')->get();
|
||||
return view('admin.requests', compact('requests'));
|
||||
$pendingProfileUpdates = \App\Models\UpdateRequest::where('status', 'pending')->count();
|
||||
|
||||
return view('admin.requests', compact(
|
||||
'requests',
|
||||
'pendingProfileUpdates'
|
||||
));
|
||||
}
|
||||
|
||||
// Approve user request
|
||||
@@ -80,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) {
|
||||
@@ -91,8 +95,18 @@ public function approveProfileUpdate($id)
|
||||
}
|
||||
}
|
||||
|
||||
// Update user table
|
||||
$user->save();
|
||||
|
||||
// Update mark_list table
|
||||
\App\Models\MarkList::where('customer_id', $user->customer_id)
|
||||
->update([
|
||||
'customer_name' => $user->customer_name,
|
||||
'company_name' => $user->company_name,
|
||||
'mobile_no' => $user->mobile_no
|
||||
]);
|
||||
|
||||
// Update request status
|
||||
$req->status = 'approved';
|
||||
$req->admin_note = 'Approved by admin on ' . now();
|
||||
$req->save();
|
||||
|
||||
964
app/Http/Controllers/ContainerController.php
Normal file
964
app/Http/Controllers/ContainerController.php
Normal file
@@ -0,0 +1,964 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Container;
|
||||
use App\Models\ContainerRow;
|
||||
use App\Models\MarkList;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use Illuminate\Http\Request;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Carbon\Carbon;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ContainerController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$containers = Container::with('rows')->latest()->get();
|
||||
|
||||
$containers->each(function ($container) {
|
||||
$rows = $container->rows;
|
||||
|
||||
$totalCtn = 0;
|
||||
$totalQty = 0;
|
||||
$totalCbm = 0;
|
||||
$totalKg = 0;
|
||||
|
||||
$ctnKeys = ['CTN', 'CTNS'];
|
||||
$qtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY', 'QTY', 'PCS', 'PIECES'];
|
||||
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
|
||||
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
|
||||
|
||||
$getFirstNumeric = function (array $data, array $possibleKeys) {
|
||||
$normalizedMap = [];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key === null || $key === '') continue;
|
||||
|
||||
$normKey = strtoupper((string)$key);
|
||||
$normKey = str_replace([' ', '/', '-', '.'], '', $normKey);
|
||||
$normalizedMap[$normKey] = $value;
|
||||
}
|
||||
|
||||
foreach ($possibleKeys as $search) {
|
||||
$normSearch = strtoupper($search);
|
||||
$normSearch = str_replace([' ', '/', '-', '.'], '', $normSearch);
|
||||
|
||||
foreach ($normalizedMap as $nKey => $value) {
|
||||
if (
|
||||
strpos($nKey, $normSearch) !== false &&
|
||||
(is_numeric($value) || (is_string($value) && is_numeric(trim($value))))
|
||||
) {
|
||||
return (float) trim($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$data = $row->data ?? [];
|
||||
|
||||
$totalCtn += $getFirstNumeric($data, $ctnKeys);
|
||||
$totalQty += $getFirstNumeric($data, $qtyKeys);
|
||||
$totalCbm += $getFirstNumeric($data, $cbmKeys);
|
||||
$totalKg += $getFirstNumeric($data, $kgKeys);
|
||||
}
|
||||
|
||||
$container->summary = [
|
||||
'total_ctn' => round($totalCtn, 2),
|
||||
'total_qty' => round($totalQty, 2),
|
||||
'total_cbm' => round($totalCbm, 3),
|
||||
'total_kg' => round($totalKg, 2),
|
||||
];
|
||||
});
|
||||
|
||||
return view('admin.container', compact('containers'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('admin.container_create');
|
||||
}
|
||||
|
||||
private function isValidExcelFormat($rows, $header)
|
||||
{
|
||||
if (empty($header) || count($rows) < 2) return false;
|
||||
|
||||
$validKeywords = [
|
||||
'MARK', 'DESCRIPTION', 'DESC', 'CTN', 'CTNS', 'QTY', 'TOTALQTY', 'ITLQTY', 'ITL QTY',
|
||||
'UNIT', 'CBM', 'TOTAL CBM', 'KG', 'TOTAL KG',
|
||||
'METAL BUCKLE', 'WATCH MOVEMENT', 'STEEL BOTTLE',
|
||||
'MEHULPAID', 'ITEM NO', 'ITEM NO.', 'SAHILPAID', 'PINAKIN', 'GST',
|
||||
'MOON LAMP', 'TRANSPARENT BOTTLE', 'PLASTIC FONDANT',
|
||||
];
|
||||
|
||||
$headerText = implode(' ', array_map('strtoupper', $header));
|
||||
$requiredHeaders = ['CTN', 'QTY', 'DESCRIPTION', 'DESC'];
|
||||
|
||||
$hasValidHeaders = false;
|
||||
foreach ($requiredHeaders as $key) {
|
||||
if (stripos($headerText, $key) !== false) {
|
||||
$hasValidHeaders = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$hasValidHeaders) return false;
|
||||
|
||||
$dataPreview = '';
|
||||
for ($i = 0; $i < min(5, count($rows)); $i++) {
|
||||
$rowText = implode(' ', array_slice($rows[$i], 0, 10));
|
||||
$dataPreview .= ' ' . strtoupper((string)$rowText);
|
||||
}
|
||||
|
||||
$validMatches = 0;
|
||||
foreach ($validKeywords as $keyword) {
|
||||
if (stripos($headerText . $dataPreview, strtoupper($keyword)) !== false) {
|
||||
$validMatches++;
|
||||
}
|
||||
}
|
||||
|
||||
return $validMatches >= 3;
|
||||
}
|
||||
|
||||
private function normalizeKey($value): string
|
||||
{
|
||||
$norm = strtoupper((string)$value);
|
||||
return str_replace([' ', '/', '-', '.'], '', $norm);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'container_name' => 'required|string',
|
||||
'container_number' => 'required|string|unique:containers,container_number',
|
||||
'container_date' => 'required|date',
|
||||
'excel_file' => 'required|file|mimes:xls,xlsx',
|
||||
]);
|
||||
|
||||
$file = $request->file('excel_file');
|
||||
$sheets = Excel::toArray([], $file);
|
||||
$rows = $sheets[0] ?? [];
|
||||
|
||||
if (count($rows) < 2) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'Excel file is empty.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
// HEADER DETECTION
|
||||
$headerRowIndex = null;
|
||||
$header = [];
|
||||
|
||||
foreach ($rows as $i => $row) {
|
||||
$trimmed = array_map(fn($v) => trim((string)$v), $row);
|
||||
$nonEmpty = array_filter($trimmed, fn($v) => $v !== '');
|
||||
if (empty($nonEmpty)) continue;
|
||||
|
||||
if (count($nonEmpty) >= 4) {
|
||||
$headerRowIndex = $i;
|
||||
$header = $trimmed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($headerRowIndex === null) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'Header row not found in Excel.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
if (!$this->isValidExcelFormat($rows, $header)) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'Only MEHUL / SAHIL / PINAKIN / GST loading list formats allowed.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
// COLUMN INDEXES
|
||||
$essentialColumns = [
|
||||
'desc_col' => null,
|
||||
'ctn_col' => null,
|
||||
'qty_col' => null,
|
||||
'totalqty_col' => null,
|
||||
'unit_col' => null,
|
||||
'price_col' => null,
|
||||
'amount_col' => null,
|
||||
'cbm_col' => null,
|
||||
'totalcbm_col' => null,
|
||||
'kg_col' => null,
|
||||
'totalkg_col' => null,
|
||||
'itemno_col' => null,
|
||||
'shopno_col' => null,
|
||||
];
|
||||
|
||||
foreach ($header as $colIndex => $headingText) {
|
||||
if (empty($headingText)) continue;
|
||||
|
||||
$normalized = $this->normalizeKey($headingText);
|
||||
|
||||
if (strpos($normalized, 'DESCRIPTION') !== false || strpos($normalized, 'DESC') !== false) {
|
||||
$essentialColumns['desc_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'CTN') !== false || strpos($normalized, 'CTNS') !== false) {
|
||||
$essentialColumns['ctn_col'] = $colIndex;
|
||||
} elseif (
|
||||
strpos($normalized, 'ITLQTY') !== false ||
|
||||
strpos($normalized, 'TOTALQTY') !== false ||
|
||||
strpos($normalized, 'TTLQTY') !== false
|
||||
) {
|
||||
$essentialColumns['totalqty_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'QTY') !== false) {
|
||||
$essentialColumns['qty_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'UNIT') !== false) {
|
||||
$essentialColumns['unit_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'PRICE') !== false) {
|
||||
$essentialColumns['price_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'AMOUNT') !== false) {
|
||||
$essentialColumns['amount_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'TOTALCBM') !== false || strpos($normalized, 'ITLCBM') !== false) {
|
||||
$essentialColumns['totalcbm_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'CBM') !== false) {
|
||||
$essentialColumns['cbm_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'TOTALKG') !== false || strpos($normalized, 'TTKG') !== false) {
|
||||
$essentialColumns['totalkg_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'KG') !== false) {
|
||||
$essentialColumns['kg_col'] = $colIndex;
|
||||
} elseif (
|
||||
strpos($normalized, 'MARKNO') !== false ||
|
||||
strpos($normalized, 'MARK') !== false ||
|
||||
strpos($normalized, 'ITEMNO') !== false ||
|
||||
strpos($normalized, 'ITEM') !== false
|
||||
) {
|
||||
$essentialColumns['itemno_col'] = $colIndex;
|
||||
} elseif (
|
||||
strpos($normalized, 'SHOPNO') !== false ||
|
||||
strpos($normalized, 'SHOP') !== false
|
||||
) {
|
||||
$essentialColumns['shopno_col'] = $colIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($essentialColumns['itemno_col'])) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'Mark / Item column not found in Excel (expected headers like MARK NO / Mark_No / Item_No).'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
// ROWS CLEANING
|
||||
$dataRows = array_slice($rows, $headerRowIndex + 1);
|
||||
$cleanedRows = [];
|
||||
$unmatchedRowsData = [];
|
||||
|
||||
foreach ($dataRows as $offset => $row) {
|
||||
$trimmedRow = array_map(fn($v) => trim((string)$v), $row);
|
||||
$nonEmptyCells = array_filter($trimmedRow, fn($v) => $v !== '');
|
||||
if (count($nonEmptyCells) < 2) continue;
|
||||
|
||||
$rowText = strtoupper(implode(' ', $trimmedRow));
|
||||
if (
|
||||
stripos($rowText, 'TOTAL') !== false ||
|
||||
stripos($rowText, 'TTL') !== false ||
|
||||
stripos($rowText, 'GRAND') !== false
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$descValue = '';
|
||||
if ($essentialColumns['desc_col'] !== null) {
|
||||
$descValue = trim($row[$essentialColumns['desc_col']] ?? '');
|
||||
}
|
||||
|
||||
if ($essentialColumns['desc_col'] !== null && $descValue === '' && count($nonEmptyCells) >= 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cleanedRows[] = [
|
||||
'row' => $row,
|
||||
'offset' => $offset,
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($cleanedRows)) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'No valid item rows found in Excel.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
// FORMULA CHECK
|
||||
$cleanNumber = function ($value) {
|
||||
if (is_string($value)) {
|
||||
$value = str_replace(',', '', trim($value));
|
||||
}
|
||||
return is_numeric($value) ? (float)$value : 0;
|
||||
};
|
||||
|
||||
$formulaErrors = [];
|
||||
|
||||
foreach ($cleanedRows as $item) {
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
|
||||
$ctn = $essentialColumns['ctn_col'] !== null ? $cleanNumber($row[$essentialColumns['ctn_col']] ?? 0) : 0;
|
||||
$qty = $essentialColumns['qty_col'] !== null ? $cleanNumber($row[$essentialColumns['qty_col']] ?? 0) : 0;
|
||||
$ttlQ = $essentialColumns['totalqty_col'] !== null ? $cleanNumber($row[$essentialColumns['totalqty_col']] ?? 0) : 0;
|
||||
$cbm = $essentialColumns['cbm_col'] !== null ? $cleanNumber($row[$essentialColumns['cbm_col']] ?? 0) : 0;
|
||||
$ttlC = $essentialColumns['totalcbm_col'] !== null ? $cleanNumber($row[$essentialColumns['totalcbm_col']] ?? 0) : 0;
|
||||
$kg = $essentialColumns['kg_col'] !== null ? $cleanNumber($row[$essentialColumns['kg_col']] ?? 0) : 0;
|
||||
$ttlK = $essentialColumns['totalkg_col'] !== null ? $cleanNumber($row[$essentialColumns['totalkg_col']] ?? 0) : 0;
|
||||
|
||||
$price = $essentialColumns['price_col'] !== null ? $cleanNumber($row[$essentialColumns['price_col']] ?? 0) : 0;
|
||||
$ttlAmount = $essentialColumns['amount_col'] !== null ? $cleanNumber($row[$essentialColumns['amount_col']] ?? 0) : 0;
|
||||
|
||||
$desc = $essentialColumns['desc_col'] !== null ? (string)($row[$essentialColumns['desc_col']] ?? '') : '';
|
||||
$mark = $essentialColumns['itemno_col'] !== null ? (string)($row[$essentialColumns['itemno_col']] ?? '') : '';
|
||||
|
||||
$expTtlQty = $qty * $ctn;
|
||||
$expTtlCbm = $cbm * $ctn;
|
||||
$expTtlKg = $kg * $ctn;
|
||||
$expTtlAmount = ($qty * $ctn) * $price;
|
||||
|
||||
$rowErrors = [];
|
||||
|
||||
if (abs($ttlQ - $expTtlQty) > 0.01) {
|
||||
$rowErrors['TOTAL QTY'] = [
|
||||
'actual' => $ttlQ,
|
||||
'expected' => $expTtlQty,
|
||||
];
|
||||
}
|
||||
|
||||
if (abs($ttlC - $expTtlCbm) > 0.0005) {
|
||||
$rowErrors['TOTAL CBM'] = [
|
||||
'actual' => $ttlC,
|
||||
'expected' => $expTtlCbm,
|
||||
];
|
||||
}
|
||||
|
||||
if (abs($ttlK - $expTtlKg) > 0.01) {
|
||||
$rowErrors['TOTAL KG'] = [
|
||||
'actual' => $ttlK,
|
||||
'expected' => $expTtlKg,
|
||||
];
|
||||
}
|
||||
|
||||
if ($essentialColumns['amount_col'] !== null && $essentialColumns['price_col'] !== null) {
|
||||
if (abs($ttlAmount - $expTtlAmount) > 0.01) {
|
||||
$rowErrors['TOTAL AMOUNT'] = [
|
||||
'actual' => $ttlAmount,
|
||||
'expected' => $expTtlAmount,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($rowErrors)) {
|
||||
$rowData = [];
|
||||
foreach ($header as $colIndex => $headingText) {
|
||||
$value = $row[$colIndex] ?? null;
|
||||
if (is_string($value)) $value = trim($value);
|
||||
$rowData[$headingText] = $value;
|
||||
}
|
||||
|
||||
$formulaErrors[] = [
|
||||
'excel_row' => $headerRowIndex + 1 + $offset,
|
||||
'mark_no' => $mark,
|
||||
'description' => $desc,
|
||||
'errors' => $rowErrors,
|
||||
'data' => $rowData,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// MARK CHECK
|
||||
$marksFromExcel = [];
|
||||
foreach ($cleanedRows as $item) {
|
||||
$row = $item['row'];
|
||||
$rawMark = $row[$essentialColumns['itemno_col']] ?? null;
|
||||
$mark = trim((string)($rawMark ?? ''));
|
||||
if ($mark !== '') {
|
||||
$marksFromExcel[] = $mark;
|
||||
}
|
||||
}
|
||||
|
||||
$marksFromExcel = array_values(array_unique($marksFromExcel));
|
||||
|
||||
if (empty($marksFromExcel)) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'No mark numbers found in Excel file.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
$validMarks = MarkList::whereIn('mark_no', $marksFromExcel)
|
||||
->where('status', 'active')
|
||||
->pluck('mark_no')
|
||||
->toArray();
|
||||
|
||||
$unmatchedMarks = array_values(array_diff($marksFromExcel, $validMarks));
|
||||
|
||||
$markErrors = [];
|
||||
|
||||
if (!empty($unmatchedMarks)) {
|
||||
foreach ($cleanedRows as $item) {
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
$rowMark = trim((string)($row[$essentialColumns['itemno_col']] ?? ''));
|
||||
|
||||
if ($rowMark === '' || !in_array($rowMark, $unmatchedMarks)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rowData = [];
|
||||
foreach ($header as $colIndex => $headingText) {
|
||||
$value = $row[$colIndex] ?? null;
|
||||
if (is_string($value)) $value = trim($value);
|
||||
$rowData[$headingText] = $value;
|
||||
}
|
||||
|
||||
$markErrors[] = [
|
||||
'excel_row' => $headerRowIndex + 1 + $offset,
|
||||
'mark_no' => $rowMark,
|
||||
'data' => $rowData,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($formulaErrors) || !empty($markErrors)) {
|
||||
return back()
|
||||
->withInput()
|
||||
->with([
|
||||
'formula_errors' => $formulaErrors,
|
||||
'mark_errors' => $markErrors,
|
||||
]);
|
||||
}
|
||||
|
||||
// STEP 1: Marks → customers mapping + grouping
|
||||
$markRecords = MarkList::whereIn('mark_no', $marksFromExcel)
|
||||
->where('status', 'active')
|
||||
->get();
|
||||
|
||||
$markToCustomerId = [];
|
||||
$markToSnapshot = [];
|
||||
|
||||
foreach ($markRecords as $mr) {
|
||||
$markToCustomerId[$mr->mark_no] = $mr->customer_id;
|
||||
|
||||
$markToSnapshot[$mr->mark_no] = [
|
||||
'customer_name' => $mr->customer_name,
|
||||
'company_name' => $mr->company_name,
|
||||
'mobile_no' => $mr->mobile_no,
|
||||
];
|
||||
}
|
||||
|
||||
$groupedByCustomer = [];
|
||||
|
||||
foreach ($cleanedRows as $item) {
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
|
||||
$rawMark = $row[$essentialColumns['itemno_col']] ?? null;
|
||||
$mark = trim((string)($rawMark ?? ''));
|
||||
|
||||
if ($mark === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$customerId = $markToCustomerId[$mark] ?? null;
|
||||
if (!$customerId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($groupedByCustomer[$customerId])) {
|
||||
$groupedByCustomer[$customerId] = [];
|
||||
}
|
||||
|
||||
$groupedByCustomer[$customerId][] = [
|
||||
'row' => $row,
|
||||
'offset' => $offset,
|
||||
'mark' => $mark,
|
||||
];
|
||||
}
|
||||
|
||||
// STEP 2: Container + ContainerRows save
|
||||
$container = Container::create([
|
||||
'container_name' => $request->container_name,
|
||||
'container_number' => $request->container_number,
|
||||
'container_date' => $request->container_date,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
$path = $file->store('containers');
|
||||
$container->update(['excel_file' => $path]);
|
||||
|
||||
$savedCount = 0;
|
||||
|
||||
foreach ($cleanedRows as $item) {
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
|
||||
$data = [];
|
||||
foreach ($header as $colIndex => $headingText) {
|
||||
$value = $row[$colIndex] ?? null;
|
||||
if (is_string($value)) $value = trim($value);
|
||||
$data[$headingText] = $value;
|
||||
}
|
||||
|
||||
ContainerRow::create([
|
||||
'container_id' => $container->id,
|
||||
'row_index' => $headerRowIndex + 1 + $offset,
|
||||
'data' => $data,
|
||||
]);
|
||||
|
||||
$savedCount++;
|
||||
}
|
||||
|
||||
// STEP 3: per-customer invoices + invoice items
|
||||
$invoiceCount = 0;
|
||||
|
||||
foreach ($groupedByCustomer as $customerId => $rowsForCustomer) {
|
||||
if (empty($rowsForCustomer)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$firstMark = $rowsForCustomer[0]['mark'];
|
||||
$snap = $markToSnapshot[$firstMark] ?? null;
|
||||
|
||||
$customerUser = \App\Models\User::where('customer_id', $customerId)->first();
|
||||
|
||||
$invoice = new Invoice();
|
||||
$invoice->container_id = $container->id;
|
||||
$invoice->customer_id = $customerUser->id ?? null;
|
||||
$invoice->mark_no = $firstMark;
|
||||
|
||||
$invoice->invoice_number = $this->generateInvoiceNumber();
|
||||
|
||||
$invoice->invoice_date = $container->container_date;
|
||||
|
||||
$invoice->due_date = Carbon::parse($invoice->invoice_date)
|
||||
->addDays(10)
|
||||
->format('Y-m-d');
|
||||
|
||||
if ($snap) {
|
||||
$invoice->customer_name = $snap['customer_name'] ?? null;
|
||||
$invoice->company_name = $snap['company_name'] ?? null;
|
||||
$invoice->customer_mobile = $snap['mobile_no'] ?? null;
|
||||
}
|
||||
|
||||
if ($customerUser) {
|
||||
$invoice->customer_email = $customerUser->email ?? null;
|
||||
$invoice->customer_address = $customerUser->address ?? null;
|
||||
$invoice->pincode = $customerUser->pincode ?? null;
|
||||
}
|
||||
|
||||
$invoice->final_amount = 0;
|
||||
$invoice->gst_percent = 0;
|
||||
$invoice->gst_amount = 0;
|
||||
$invoice->final_amount_with_gst = 0;
|
||||
|
||||
$uniqueMarks = array_unique(array_column($rowsForCustomer, 'mark'));
|
||||
$invoice->notes = 'Auto-created from Container ' . $container->container_number
|
||||
. ' for Mark(s): ' . implode(', ', $uniqueMarks);
|
||||
$invoice->pdf_path = null;
|
||||
$invoice->status = 'pending';
|
||||
|
||||
$invoice->save();
|
||||
$invoiceCount++;
|
||||
|
||||
foreach ($rowsForCustomer as $item) {
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
$mark = $item['mark']; // ✅ mark_no from Excel
|
||||
|
||||
$description = $essentialColumns['desc_col'] !== null ? ($row[$essentialColumns['desc_col']] ?? null) : null;
|
||||
$ctn = $essentialColumns['ctn_col'] !== null ? (int) ($row[$essentialColumns['ctn_col']] ?? 0) : 0;
|
||||
$qty = $essentialColumns['qty_col'] !== null ? (int) ($row[$essentialColumns['qty_col']] ?? 0) : 0;
|
||||
$ttlQty = $essentialColumns['totalqty_col'] !== null ? (int) ($row[$essentialColumns['totalqty_col']] ?? 0) : $qty;
|
||||
$unit = $essentialColumns['unit_col'] !== null ? ($row[$essentialColumns['unit_col']] ?? null) : null;
|
||||
$price = $essentialColumns['price_col'] !== null ? (float) ($row[$essentialColumns['price_col']] ?? 0) : 0;
|
||||
$ttlAmount = $essentialColumns['amount_col'] !== null ? (float) ($row[$essentialColumns['amount_col']] ?? 0) : 0;
|
||||
$cbm = $essentialColumns['cbm_col'] !== null ? (float) ($row[$essentialColumns['cbm_col']] ?? 0) : 0;
|
||||
$ttlCbm = $essentialColumns['totalcbm_col'] !== null ? (float) ($row[$essentialColumns['totalcbm_col']] ?? $cbm) : $cbm;
|
||||
$kg = $essentialColumns['kg_col'] !== null ? (float) ($row[$essentialColumns['kg_col']] ?? 0) : 0;
|
||||
$ttlKg = $essentialColumns['totalkg_col'] !== null ? (float) ($row[$essentialColumns['totalkg_col']] ?? $kg) : $kg;
|
||||
|
||||
$shopNo = $essentialColumns['shopno_col'] !== null ? ($row[$essentialColumns['shopno_col']] ?? null) : null;
|
||||
|
||||
$rowIndex = $headerRowIndex + 1 + $offset;
|
||||
|
||||
InvoiceItem::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
'container_id' => $container->id,
|
||||
'container_row_index' => $rowIndex,
|
||||
'description' => $description,
|
||||
'ctn' => $ctn,
|
||||
'qty' => $qty,
|
||||
'ttl_qty' => $ttlQty,
|
||||
'unit' => $unit,
|
||||
'price' => $price,
|
||||
'ttl_amount' => $ttlAmount,
|
||||
'cbm' => $cbm,
|
||||
'ttl_cbm' => $ttlCbm,
|
||||
'kg' => $kg,
|
||||
'ttl_kg' => $ttlKg,
|
||||
'shop_no' => $shopNo,
|
||||
'mark_no' => $mark, // ✅ save mark_no from Excel
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$msg = "Container '{$container->container_number}' created with {$savedCount} rows and {$invoiceCount} customer invoice(s).";
|
||||
return redirect()->route('containers.index')->with('success', $msg);
|
||||
}
|
||||
|
||||
public function show(Container $container)
|
||||
{
|
||||
$container->load('rows');
|
||||
|
||||
$lockedRowIndexes = \App\Models\Invoice::whereIn('invoices.status', ['paid', 'paying'])
|
||||
->where('invoices.container_id', $container->id)
|
||||
->join('invoice_items', 'invoices.id', '=', 'invoice_items.invoice_id')
|
||||
->pluck('invoice_items.container_row_index')
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return view('admin.container_show', compact('container', 'lockedRowIndexes'));
|
||||
}
|
||||
|
||||
public function updateRows(Request $request, Container $container)
|
||||
{
|
||||
$rowsInput = $request->input('rows', []);
|
||||
|
||||
foreach ($rowsInput as $rowId => $cols) {
|
||||
$row = ContainerRow::where('container_id', $container->id)
|
||||
->where('id', $rowId)
|
||||
->first();
|
||||
|
||||
if (!$row) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = $row->data ?? [];
|
||||
foreach ($cols as $colHeader => $value) {
|
||||
$data[$colHeader] = $value;
|
||||
}
|
||||
$row->update(['data' => $data]);
|
||||
|
||||
$normalizedMap = [];
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key === null || $key === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normKey = strtoupper((string)$key);
|
||||
$normKey = str_replace([' ', '/', '-', '.'], '', $normKey);
|
||||
$normalizedMap[$normKey] = $value;
|
||||
}
|
||||
|
||||
$getFirstNumeric = function (array $map, array $possibleKeys) {
|
||||
foreach ($possibleKeys as $search) {
|
||||
$normSearch = strtoupper($search);
|
||||
$normSearch = str_replace([' ', '/', '-', '.'], '', $normSearch);
|
||||
|
||||
foreach ($map as $nKey => $value) {
|
||||
if (strpos($nKey, $normSearch) !== false) {
|
||||
if (is_numeric($value)) {
|
||||
return (float)$value;
|
||||
}
|
||||
if (is_string($value) && is_numeric(trim($value))) {
|
||||
return (float)trim($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
$ctnKeys = ['CTN', 'CTNS'];
|
||||
$qtyKeys = ['QTY', 'PCS', 'PIECES'];
|
||||
$ttlQtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY'];
|
||||
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
|
||||
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
|
||||
$amountKeys = ['AMOUNT', 'TTLAMOUNT', 'TOTALAMOUNT'];
|
||||
|
||||
$ctn = $getFirstNumeric($normalizedMap, $ctnKeys);
|
||||
|
||||
$qty = $getFirstNumeric($normalizedMap, $qtyKeys);
|
||||
|
||||
$ttlQ = $getFirstNumeric($normalizedMap, $ttlQtyKeys);
|
||||
|
||||
if ($ttlQ == 0 && $ctn && $qty) {
|
||||
$ttlQ = $ctn * $qty;
|
||||
}
|
||||
|
||||
$cbm = $getFirstNumeric($normalizedMap, ['CBM']);
|
||||
$ttlC = $getFirstNumeric($normalizedMap, ['TOTALCBM', 'TTLCBM', 'ITLCBM']);
|
||||
if ($ttlC == 0 && $cbm && $ctn) {
|
||||
$ttlC = $cbm * $ctn;
|
||||
}
|
||||
|
||||
$kg = $getFirstNumeric($normalizedMap, ['KG', 'WEIGHT']);
|
||||
$ttlK = $getFirstNumeric($normalizedMap, ['TOTALKG', 'TTKG']);
|
||||
if ($ttlK == 0 && $kg && $ctn) {
|
||||
$ttlK = $kg * $ctn;
|
||||
}
|
||||
|
||||
$price = $getFirstNumeric($normalizedMap, ['PRICE', 'RATE']);
|
||||
$amount = $getFirstNumeric($normalizedMap, $amountKeys);
|
||||
if ($amount == 0 && $price && $ttlQ) {
|
||||
$amount = $price * $ttlQ;
|
||||
}
|
||||
|
||||
$desc = null;
|
||||
foreach (['DESCRIPTION', 'DESC'] as $dKey) {
|
||||
$normD = str_replace([' ', '/', '-', '.'], '', strtoupper($dKey));
|
||||
foreach ($normalizedMap as $nKey => $v) {
|
||||
if (strpos($nKey, $normD) !== false) {
|
||||
$desc = is_string($v) ? trim($v) : $v;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$shopNo = null;
|
||||
foreach (['SHOPNO', 'SHOP'] as $sKey) {
|
||||
$normS = str_replace([' ', '/', '-', '.'], '', strtoupper($sKey));
|
||||
foreach ($normalizedMap as $nKey => $v) {
|
||||
if (strpos($nKey, $normS) !== false) {
|
||||
$shopNo = is_string($v) ? trim($v) : $v;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Get mark_no
|
||||
$markNo = null;
|
||||
foreach (['MARKNO', 'MARK', 'ITEMNO', 'ITEM'] as $mKey) {
|
||||
$normM = str_replace([' ', '/', '-', '.'], '', strtoupper($mKey));
|
||||
foreach ($normalizedMap as $nKey => $v) {
|
||||
if (strpos($nKey, $normM) !== false) {
|
||||
$markNo = is_string($v) ? trim($v) : $v;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rowIndex = $row->row_index;
|
||||
|
||||
$items = InvoiceItem::where('container_id', $container->id)
|
||||
->where('container_row_index', $rowIndex)
|
||||
->get();
|
||||
|
||||
if ($items->isEmpty() && $desc) {
|
||||
$items = InvoiceItem::where('container_id', $container->id)
|
||||
->whereNull('container_row_index')
|
||||
->where('description', $desc)
|
||||
->get();
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
$item->description = $desc;
|
||||
$item->ctn = $ctn;
|
||||
$item->qty = $qty;
|
||||
$item->ttl_qty = $ttlQ;
|
||||
$item->price = $price;
|
||||
$item->ttl_amount = $amount;
|
||||
$item->cbm = $cbm;
|
||||
$item->ttl_cbm = $ttlC;
|
||||
$item->kg = $kg;
|
||||
$item->ttl_kg = $ttlK;
|
||||
$item->shop_no = $shopNo;
|
||||
$item->mark_no = $markNo; // ✅ update mark_no
|
||||
$item->save();
|
||||
|
||||
$invoice = $item->invoice;
|
||||
if ($invoice) {
|
||||
$newBaseAmount = InvoiceItem::where('invoice_id', $invoice->id)
|
||||
->sum('ttl_amount');
|
||||
|
||||
$taxType = $invoice->tax_type;
|
||||
$cgstPercent = (float) ($invoice->cgst_percent ?? 0);
|
||||
$sgstPercent = (float) ($invoice->sgst_percent ?? 0);
|
||||
$igstPercent = (float) ($invoice->igst_percent ?? 0);
|
||||
|
||||
$gstPercent = 0;
|
||||
if ($taxType === 'gst') {
|
||||
$gstPercent = $cgstPercent + $sgstPercent;
|
||||
} elseif ($taxType === 'igst') {
|
||||
$gstPercent = $igstPercent;
|
||||
}
|
||||
|
||||
$gstAmount = $newBaseAmount * $gstPercent / 100;
|
||||
$finalWithGst = $newBaseAmount + $gstAmount;
|
||||
|
||||
$invoice->final_amount = $newBaseAmount;
|
||||
$invoice->gst_amount = $gstAmount;
|
||||
$invoice->final_amount_with_gst = $finalWithGst;
|
||||
$invoice->gst_percent = $gstPercent;
|
||||
$invoice->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('containers.show', $container->id)
|
||||
->with('success', 'Excel rows updated successfully.');
|
||||
}
|
||||
|
||||
public function updateStatus(Request $request, Container $container)
|
||||
{
|
||||
$request->validate([
|
||||
'status' => 'required|in:container-ready,export-custom,international-transit,arrived-at-india,import-custom,warehouse,domestic-distribution,out-for-delivery,delivered',
|
||||
]);
|
||||
|
||||
$container->status = $request->status;
|
||||
$container->save();
|
||||
|
||||
if ($request->wantsJson() || $request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'status' => $container->status,
|
||||
]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Container status updated.');
|
||||
}
|
||||
|
||||
public function destroy(Container $container)
|
||||
{
|
||||
$container->delete();
|
||||
|
||||
if (request()->wantsJson() || request()->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Container deleted',
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('containers.index')
|
||||
->with('success', 'Container deleted.');
|
||||
}
|
||||
|
||||
private function generateInvoiceNumber(): string
|
||||
{
|
||||
$year = now()->format('Y');
|
||||
|
||||
$last = Invoice::whereYear('created_at', $year)
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
if ($last) {
|
||||
$parts = explode('-', $last->invoice_number);
|
||||
$seq = 0;
|
||||
|
||||
if (count($parts) === 3) {
|
||||
$seq = (int) $parts[2];
|
||||
}
|
||||
|
||||
$nextSeq = $seq + 1;
|
||||
} else {
|
||||
$nextSeq = 1;
|
||||
}
|
||||
|
||||
return 'INV-' . $year . '-' . str_pad($nextSeq, 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
public function downloadPdf(Container $container)
|
||||
{
|
||||
$container->load('rows');
|
||||
|
||||
$pdf = Pdf::loadView('admin.container_pdf', [
|
||||
'container' => $container,
|
||||
])->setPaper('a4', 'landscape');
|
||||
|
||||
$fileName = 'container-'.$container->container_number.'.pdf';
|
||||
|
||||
return $pdf->download($fileName);
|
||||
}
|
||||
|
||||
public function downloadExcel(Container $container)
|
||||
{
|
||||
if (!$container->excel_file) {
|
||||
abort(404, 'Excel file not found on record.');
|
||||
}
|
||||
|
||||
$path = $container->excel_file;
|
||||
|
||||
if (!Storage::exists($path)) {
|
||||
abort(404, 'Excel file missing on server.');
|
||||
}
|
||||
|
||||
$fileName = 'container-'.$container->container_number.'.xlsx';
|
||||
|
||||
return Storage::download($path, $fileName);
|
||||
}
|
||||
|
||||
public function popupPopup(Container $container)
|
||||
{
|
||||
$container->load('rows');
|
||||
|
||||
$rows = $container->rows ?? collect();
|
||||
|
||||
$totalCtn = 0;
|
||||
$totalQty = 0;
|
||||
$totalCbm = 0;
|
||||
$totalKg = 0;
|
||||
|
||||
$ctnKeys = ['CTN', 'CTNS'];
|
||||
$qtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY', 'QTY', 'PCS', 'PIECES'];
|
||||
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
|
||||
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
|
||||
|
||||
$getFirstNumeric = function (array $data, array $possibleKeys) {
|
||||
$normalizedMap = [];
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key === null) continue;
|
||||
$normKey = strtoupper((string)$key);
|
||||
$normKey = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normKey);
|
||||
$normalizedMap[$normKey] = $value;
|
||||
}
|
||||
|
||||
foreach ($possibleKeys as $search) {
|
||||
$normSearch = strtoupper($search);
|
||||
$normSearch = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normSearch);
|
||||
|
||||
foreach ($normalizedMap as $nKey => $value) {
|
||||
if (strpos($nKey, $normSearch) !== false) {
|
||||
if (is_numeric($value)) {
|
||||
return (float)$value;
|
||||
}
|
||||
if (is_string($value) && is_numeric(trim($value))) {
|
||||
return (float)trim($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$data = $row->data ?? [];
|
||||
if (!is_array($data)) continue;
|
||||
|
||||
$totalCtn += $getFirstNumeric($data, $ctnKeys);
|
||||
$totalQty += $getFirstNumeric($data, $qtyKeys);
|
||||
$totalCbm += $getFirstNumeric($data, $cbmKeys);
|
||||
$totalKg += $getFirstNumeric($data, $kgKeys);
|
||||
}
|
||||
|
||||
$summary = [
|
||||
'total_ctn' => round($totalCtn, 2),
|
||||
'total_qty' => round($totalQty, 2),
|
||||
'total_cbm' => round($totalCbm, 3),
|
||||
'total_kg' => round($totalKg, 2),
|
||||
];
|
||||
|
||||
return view('admin.partials.container_popup_readonly', [
|
||||
'container' => $container,
|
||||
'summary' => $summary,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -21,23 +21,33 @@ class UserOrderController extends Controller
|
||||
}
|
||||
|
||||
// -------------------------------------
|
||||
// Get all orders
|
||||
// Get customer invoices with containers
|
||||
// -------------------------------------
|
||||
$orders = $user->orders()->with('invoice')->get();
|
||||
$invoices = $user->invoices()->with('container')->get();
|
||||
|
||||
// Unique containers for this customer
|
||||
$containers = $invoices->pluck('container')->filter()->unique('id');
|
||||
|
||||
// -------------------------------------
|
||||
// Counts
|
||||
// Counts based on container status
|
||||
// -------------------------------------
|
||||
$totalOrders = $orders->count();
|
||||
$delivered = $orders->where('status', 'delivered')->count();
|
||||
$inTransit = $orders->where('status', '!=', 'delivered')->count();
|
||||
$active = $totalOrders;
|
||||
$totalOrders = $containers->count();
|
||||
|
||||
$delivered = $containers->where('status', 'delivered')->count();
|
||||
|
||||
$inTransit = $containers->whereNotIn('status', [
|
||||
'delivered',
|
||||
'warehouse',
|
||||
'domestic-distribution'
|
||||
])->count();
|
||||
|
||||
$active = $totalOrders;
|
||||
|
||||
// -------------------------------------
|
||||
// Total Amount = Invoice.total_with_gst
|
||||
// Total Amount = sum of invoice totals
|
||||
// -------------------------------------
|
||||
$totalAmount = $orders->sum(function ($o) {
|
||||
return $o->invoice->final_amount_with_gst ?? 0;
|
||||
$totalAmount = $invoices->sum(function ($invoice) {
|
||||
return $invoice->final_amount_with_gst ?? 0;
|
||||
});
|
||||
|
||||
// Format total amount in K, L, Cr
|
||||
@@ -45,13 +55,12 @@ class UserOrderController extends Controller
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
|
||||
'summary' => [
|
||||
'active_orders' => $active,
|
||||
'in_transit_orders' => $inTransit,
|
||||
'delivered_orders' => $delivered,
|
||||
'total_value' => $formattedAmount, // formatted value
|
||||
'total_raw' => $totalAmount // original value
|
||||
'total_value' => $formattedAmount,
|
||||
'total_raw' => $totalAmount
|
||||
]
|
||||
]);
|
||||
}
|
||||
@@ -90,20 +99,28 @@ class UserOrderController extends Controller
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Fetch orders for this user
|
||||
$orders = $user->orders()
|
||||
->with(['invoice', 'shipments'])
|
||||
// Get invoices with containers for this customer
|
||||
$invoices = $user->invoices()
|
||||
->with('container')
|
||||
->orderBy('id', 'desc')
|
||||
->get()
|
||||
->map(function ($o) {
|
||||
return [
|
||||
'order_id' => $o->order_id,
|
||||
'status' => $o->status,
|
||||
'amount' => $o->ttl_amount,
|
||||
'description'=> "Order from {$o->origin} to {$o->destination}",
|
||||
'created_at' => $o->created_at,
|
||||
];
|
||||
});
|
||||
->get();
|
||||
|
||||
// Extract unique containers
|
||||
$containers = $invoices->pluck('container')
|
||||
->filter()
|
||||
->unique('id')
|
||||
->values();
|
||||
|
||||
$orders = $containers->map(function ($container) {
|
||||
|
||||
return [
|
||||
'order_id' => $container->id,
|
||||
'container_number' => $container->container_number,
|
||||
'status' => $container->status,
|
||||
'container_date' => $container->container_date,
|
||||
'created_at' => $container->created_at,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
@@ -115,45 +132,73 @@ public function orderDetails($order_id)
|
||||
{
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
$order = $user->orders()
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Find container first
|
||||
$container = \App\Models\Container::find($order_id);
|
||||
|
||||
if (!$container) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Container not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Find invoice belonging to this user for this container
|
||||
$invoice = \App\Models\Invoice::where('customer_id', $user->id)
|
||||
->where('container_id', $container->id)
|
||||
->with(['items'])
|
||||
->where('order_id', $order_id)
|
||||
->first();
|
||||
|
||||
if (!$order) {
|
||||
return response()->json(['success' => false, 'message' => 'Order not found'], 404);
|
||||
if (!$invoice) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Order not found for this user'
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'order' => $order
|
||||
'order' => [
|
||||
'container_id' => $container->id,
|
||||
'container_number' => $container->container_number,
|
||||
'container_date' => $container->container_date,
|
||||
'status' => $container->status,
|
||||
'invoice_id' => $invoice->id,
|
||||
'items' => $invoice->items
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function orderShipment($order_id)
|
||||
{
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
// public function orderShipment($order_id)
|
||||
// {
|
||||
// $user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
// Get order
|
||||
$order = $user->orders()->where('order_id', $order_id)->first();
|
||||
// // Get order
|
||||
// $order = $user->orders()->where('order_id', $order_id)->first();
|
||||
|
||||
if (!$order) {
|
||||
return response()->json(['success' => false, 'message' => 'Order not found'], 404);
|
||||
}
|
||||
// if (!$order) {
|
||||
// return response()->json(['success' => false, 'message' => 'Order not found'], 404);
|
||||
// }
|
||||
|
||||
// Find shipment only for this order
|
||||
$shipment = $order->shipments()
|
||||
->with(['items' => function ($q) use ($order) {
|
||||
$q->where('order_id', $order->id);
|
||||
}])
|
||||
->first();
|
||||
// // Find shipment only for this order
|
||||
// $shipment = $order->shipments()
|
||||
// ->with(['items' => function ($q) use ($order) {
|
||||
// $q->where('order_id', $order->id);
|
||||
// }])
|
||||
// ->first();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'shipment' => $shipment
|
||||
]);
|
||||
}
|
||||
// return response()->json([
|
||||
// 'success' => true,
|
||||
// 'shipment' => $shipment
|
||||
// ]);
|
||||
// }
|
||||
|
||||
|
||||
public function orderInvoice($order_id)
|
||||
@@ -179,23 +224,35 @@ public function trackOrder($order_id)
|
||||
{
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
$order = $user->orders()
|
||||
->with('shipments')
|
||||
->where('order_id', $order_id)
|
||||
->first();
|
||||
|
||||
if (!$order) {
|
||||
return response()->json(['success' => false, 'message' => 'Order not found'], 404);
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 401);
|
||||
}
|
||||
|
||||
$shipment = $order->shipments()->first();
|
||||
// Ensure the container belongs to this customer via invoice
|
||||
$invoice = \App\Models\Invoice::where('customer_id', $user->id)
|
||||
->where('container_id', $order_id)
|
||||
->with('container')
|
||||
->first();
|
||||
|
||||
if (!$invoice || !$invoice->container) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Order not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$container = $invoice->container;
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'track' => [
|
||||
'order_id' => $order->order_id,
|
||||
'shipment_status' => $shipment->status ?? 'pending',
|
||||
'shipment_date' => $shipment->shipment_date ?? null,
|
||||
'order_id' => $container->id,
|
||||
'container_number' => $container->container_number,
|
||||
'status' => $container->status,
|
||||
'container_date' => $container->container_date,
|
||||
]
|
||||
]);
|
||||
}
|
||||
@@ -289,6 +346,44 @@ public function invoiceDetails($invoice_id)
|
||||
]);
|
||||
}
|
||||
|
||||
// public function confirmOrder($order_id)
|
||||
// {
|
||||
// $user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
// if (! $user) {
|
||||
// return response()->json([
|
||||
// 'success' => false,
|
||||
// 'message' => 'Unauthorized'
|
||||
// ], 401);
|
||||
// }
|
||||
|
||||
// $order = $user->orders()
|
||||
// ->where('order_id', $order_id)
|
||||
// ->first();
|
||||
|
||||
// if (! $order) {
|
||||
// return response()->json([
|
||||
// 'success' => false,
|
||||
// 'message' => 'Order not found'
|
||||
// ], 404);
|
||||
// }
|
||||
|
||||
// // 🚫 Only allow confirm from order_placed
|
||||
// if ($order->status !== 'order_placed') {
|
||||
// return response()->json([
|
||||
// 'success' => false,
|
||||
// 'message' => 'Order cannot be confirmed'
|
||||
// ], 422);
|
||||
// }
|
||||
|
||||
// $order->status = 'order_confirmed';
|
||||
// $order->save();
|
||||
|
||||
// return response()->json([
|
||||
// 'success' => true,
|
||||
// 'message' => 'Order confirmed successfully'
|
||||
// ]);
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
26
app/Imports/OrderItemsPreviewImport.php
Normal file
26
app/Imports/OrderItemsPreviewImport.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace App\Imports;
|
||||
|
||||
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class OrderItemsPreviewImport implements ToCollection
|
||||
{
|
||||
public array $rows = [];
|
||||
|
||||
public function collection(Collection $collection)
|
||||
{
|
||||
$header = $collection->first()->map(fn ($h) => strtolower(trim($h)))->toArray();
|
||||
|
||||
foreach ($collection->skip(1) as $row) {
|
||||
$item = [];
|
||||
foreach ($header as $i => $key) {
|
||||
$item[$key] = $row[$i] ?? null;
|
||||
}
|
||||
|
||||
if (!empty($item['description'])) {
|
||||
$this->rows[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ class Admin extends Authenticatable
|
||||
'name', 'email', 'password', 'username',
|
||||
'phone', 'emergency_phone', 'address',
|
||||
'role', 'department', 'designation', 'joining_date',
|
||||
'status', 'additional_info', 'type', // admin/staff indicator
|
||||
'status', 'additional_info', 'type','employee_id', // admin/staff indicator
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
|
||||
@@ -20,8 +20,6 @@ class ChatMessage extends Model
|
||||
'read_by_user',
|
||||
'client_id',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* The ticket this message belongs to.
|
||||
*/
|
||||
|
||||
30
app/Models/Container.php
Normal file
30
app/Models/Container.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Container extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'container_name',
|
||||
'container_number',
|
||||
'container_date',
|
||||
'status',
|
||||
'excel_file',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'container_date' => 'date',
|
||||
];
|
||||
|
||||
public function rows()
|
||||
{
|
||||
return $this->hasMany(ContainerRow::class);
|
||||
}
|
||||
|
||||
public function invoices()
|
||||
{
|
||||
return $this->hasMany(Invoice::class);
|
||||
}
|
||||
}
|
||||
23
app/Models/ContainerRow.php
Normal file
23
app/Models/ContainerRow.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ContainerRow extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'container_id',
|
||||
'row_index',
|
||||
'data',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'data' => 'array',
|
||||
];
|
||||
|
||||
public function container()
|
||||
{
|
||||
return $this->belongsTo(Container::class);
|
||||
}
|
||||
}
|
||||
@@ -9,41 +9,36 @@ class Invoice extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'order_id',
|
||||
'customer_id',
|
||||
'mark_no',
|
||||
|
||||
'invoice_number',
|
||||
'invoice_date',
|
||||
'due_date',
|
||||
|
||||
'payment_method',
|
||||
'reference_no',
|
||||
'status',
|
||||
|
||||
'final_amount', // without tax
|
||||
|
||||
'tax_type', // gst / igst
|
||||
'gst_percent', // only used for gst UI input
|
||||
'cgst_percent',
|
||||
'sgst_percent',
|
||||
'igst_percent',
|
||||
|
||||
'gst_amount', // total tax amount
|
||||
'final_amount_with_gst',
|
||||
|
||||
'customer_name',
|
||||
'company_name',
|
||||
'customer_email',
|
||||
'customer_mobile',
|
||||
'customer_address',
|
||||
'pincode',
|
||||
|
||||
'pdf_path',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'container_id',
|
||||
'customer_id',
|
||||
'mark_no',
|
||||
'invoice_number',
|
||||
'invoice_date',
|
||||
'due_date',
|
||||
'payment_method',
|
||||
'reference_no',
|
||||
'status',
|
||||
'final_amount',
|
||||
'gst_percent',
|
||||
'gst_amount',
|
||||
'final_amount_with_gst',
|
||||
'customer_name',
|
||||
'company_name',
|
||||
'customer_email',
|
||||
'customer_mobile',
|
||||
'customer_address',
|
||||
'pincode',
|
||||
'pdf_path',
|
||||
'notes',
|
||||
// totals from charge groups
|
||||
'charge_groups_total',
|
||||
'grand_total_with_charges',
|
||||
'tax_type',
|
||||
'cgst_percent',
|
||||
'sgst_percent',
|
||||
'igst_percent',
|
||||
];
|
||||
|
||||
/****************************
|
||||
* Relationships
|
||||
@@ -54,29 +49,38 @@ class Invoice extends Model
|
||||
return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC');
|
||||
}
|
||||
|
||||
public function order()
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
}
|
||||
// public function container()
|
||||
// {
|
||||
// return $this->belongsTo(Container::class);
|
||||
// }
|
||||
|
||||
public function customer()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'customer_id');
|
||||
}
|
||||
|
||||
public function installments()
|
||||
{
|
||||
return $this->hasMany(InvoiceInstallment::class);
|
||||
}
|
||||
|
||||
public function chargeGroups()
|
||||
{
|
||||
return $this->hasMany(InvoiceChargeGroup::class, 'invoice_id');
|
||||
}
|
||||
|
||||
/****************************
|
||||
* Helper Functions
|
||||
****************************/
|
||||
|
||||
// Auto calculate GST fields (you can call this in controller before saving)
|
||||
// (Items based calculateTotals वापरणार नाहीस तरी ठेवू शकतोस)
|
||||
public function calculateTotals()
|
||||
{
|
||||
$gst = ($this->final_amount * $this->gst_percent) / 100;
|
||||
$this->gst_amount = $gst;
|
||||
$this->gst_amount = $gst;
|
||||
$this->final_amount_with_gst = $this->final_amount + $gst;
|
||||
}
|
||||
|
||||
// Check overdue status condition
|
||||
public function isOverdue()
|
||||
{
|
||||
return $this->status === 'pending' && now()->gt($this->due_date);
|
||||
@@ -84,13 +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');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
31
app/Models/InvoiceChargeGroup.php
Normal file
31
app/Models/InvoiceChargeGroup.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class InvoiceChargeGroup extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'invoice_id',
|
||||
'group_name',
|
||||
'basis_type',
|
||||
'basis_value',
|
||||
'rate',
|
||||
'total_charge',
|
||||
|
||||
'tax_type',
|
||||
'gst_percent',
|
||||
'total_with_gst',
|
||||
];
|
||||
|
||||
public function invoice()
|
||||
{
|
||||
return $this->belongsTo(Invoice::class);
|
||||
}
|
||||
|
||||
public function items()
|
||||
{
|
||||
return $this->hasMany(InvoiceChargeGroupItem::class, 'group_id');
|
||||
}
|
||||
}
|
||||
23
app/Models/InvoiceChargeGroupItem.php
Normal file
23
app/Models/InvoiceChargeGroupItem.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class InvoiceChargeGroupItem extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'group_id',
|
||||
'invoice_item_id',
|
||||
];
|
||||
|
||||
public function group()
|
||||
{
|
||||
return $this->belongsTo(InvoiceChargeGroup::class, 'group_id');
|
||||
}
|
||||
|
||||
public function item()
|
||||
{
|
||||
return $this->belongsTo(InvoiceItem::class, 'invoice_item_id');
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ class InvoiceItem extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'invoice_id',
|
||||
'container_id', // Container mapping
|
||||
'container_row_index', // Container row index
|
||||
|
||||
'description',
|
||||
'ctn',
|
||||
@@ -27,6 +29,7 @@ class InvoiceItem extends Model
|
||||
'ttl_kg',
|
||||
|
||||
'shop_no',
|
||||
'mark_no',
|
||||
];
|
||||
|
||||
/****************************
|
||||
@@ -37,4 +40,79 @@ class InvoiceItem extends Model
|
||||
{
|
||||
return $this->belongsTo(Invoice::class);
|
||||
}
|
||||
|
||||
public function chargeGroupItems()
|
||||
{
|
||||
return $this->hasMany(InvoiceChargeGroupItem::class, 'invoice_item_id');
|
||||
}
|
||||
|
||||
// हे helper: पहिला group fetch करून त्यावरून rate/total काढणे
|
||||
public function getChargeRateAttribute()
|
||||
{
|
||||
$pivot = $this->chargeGroupItems->first();
|
||||
if (!$pivot || !$pivot->group) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$group = $pivot->group;
|
||||
|
||||
// basis नुसार या item चा basis value
|
||||
$basis = 0;
|
||||
switch ($group->basis_type) {
|
||||
case 'ttl_qty':
|
||||
$basis = $this->ttl_qty;
|
||||
break;
|
||||
case 'amount':
|
||||
$basis = $this->ttl_amount;
|
||||
break;
|
||||
case 'ttl_cbm':
|
||||
$basis = $this->ttl_cbm;
|
||||
break;
|
||||
case 'ttl_kg':
|
||||
$basis = $this->ttl_kg;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($basis <= 0 || ($group->basis_value ?? 0) <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// group चा rate field आधीच आहे, ते direct वापरू
|
||||
return (float) $group->rate;
|
||||
}
|
||||
|
||||
public function getChargeTotalAttribute()
|
||||
{
|
||||
$pivot = $this->chargeGroupItems->first();
|
||||
if (!$pivot || !$pivot->group) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$group = $pivot->group;
|
||||
|
||||
$basis = 0;
|
||||
switch ($group->basis_type) {
|
||||
case 'ttl_qty':
|
||||
$basis = $this->ttl_qty;
|
||||
break;
|
||||
case 'amount':
|
||||
$basis = $this->ttl_amount;
|
||||
break;
|
||||
case 'ttl_cbm':
|
||||
$basis = $this->ttl_cbm;
|
||||
break;
|
||||
case 'ttl_kg':
|
||||
$basis = $this->ttl_kg;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($basis <= 0 || ($group->basis_value ?? 0) <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// per unit rate
|
||||
$rate = (float) $group->rate;
|
||||
// item total = basis * rate
|
||||
return $basis * $rate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,11 +58,31 @@ class Order extends Model
|
||||
return $this->belongsToMany(\App\Models\Shipment::class, 'shipment_items', 'order_id', 'shipment_id');
|
||||
}
|
||||
|
||||
public function invoice()
|
||||
// public function invoice()
|
||||
// {
|
||||
// return $this->hasOne(\App\Models\Invoice::class, 'order_id', 'id');
|
||||
// }
|
||||
|
||||
|
||||
const STATUS_LABELS = [
|
||||
'order_placed' => 'Order Placed',
|
||||
'order_confirmed' => 'Order Confirmed',
|
||||
'supplier_warehouse' => 'Supplier Warehouse',
|
||||
'consolidate_warehouse'=> 'Consolidate Warehouse',
|
||||
'export_custom' => 'Export Custom',
|
||||
'international_transit'=> 'International Transit',
|
||||
'arrived_india' => 'Arrived at India',
|
||||
'import_custom' => 'Import Custom',
|
||||
'warehouse' => 'Warehouse',
|
||||
'domestic_distribution'=> 'Domestic Distribution',
|
||||
'out_for_delivery' => 'Out for Delivery',
|
||||
'delivered' => 'Delivered',
|
||||
];
|
||||
|
||||
public function getStatusLabelAttribute()
|
||||
{
|
||||
return $this->hasOne(\App\Models\Invoice::class, 'order_id', 'id');
|
||||
return self::STATUS_LABELS[$this->status]
|
||||
?? ucfirst(str_replace('_', ' ', $this->status));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -45,25 +45,6 @@ class Shipment extends Model
|
||||
return $this->belongsToMany(Order::class, 'shipment_items', 'shipment_id', 'order_id');
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// STATUS CONSTANTS
|
||||
// ---------------------------
|
||||
|
||||
const STATUS_PENDING = 'pending';
|
||||
const STATUS_IN_TRANSIT = 'in_transit';
|
||||
const STATUS_DISPATCHED = 'dispatched';
|
||||
const STATUS_DELIVERED = 'delivered';
|
||||
|
||||
public static function statusOptions()
|
||||
{
|
||||
return [
|
||||
self::STATUS_PENDING => 'Pending',
|
||||
self::STATUS_IN_TRANSIT => 'In Transit',
|
||||
self::STATUS_DISPATCHED => 'Dispatched',
|
||||
self::STATUS_DELIVERED => 'Delivered',
|
||||
];
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// HELPERS
|
||||
// ---------------------------
|
||||
@@ -73,8 +54,38 @@ class Shipment extends Model
|
||||
return $this->items()->count();
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// STATUS CONSTANTS (LOGISTICS FLOW)
|
||||
// ---------------------------
|
||||
const STATUS_SHIPMENT_READY = 'shipment_ready';
|
||||
const STATUS_EXPORT_CUSTOM = 'export_custom';
|
||||
const STATUS_INTERNATIONAL_TRANSIT= 'international_transit';
|
||||
const STATUS_ARRIVED_INDIA = 'arrived_india';
|
||||
const STATUS_IMPORT_CUSTOM = 'import_custom';
|
||||
const STATUS_WAREHOUSE = 'warehouse';
|
||||
const STATUS_DOMESTIC_DISTRIBUTION= 'domestic_distribution';
|
||||
const STATUS_OUT_FOR_DELIVERY = 'out_for_delivery';
|
||||
const STATUS_DELIVERED = 'delivered';
|
||||
|
||||
public static function statusOptions()
|
||||
{
|
||||
return [
|
||||
self::STATUS_SHIPMENT_READY => 'Shipment Ready',
|
||||
self::STATUS_EXPORT_CUSTOM => 'Export Custom',
|
||||
self::STATUS_INTERNATIONAL_TRANSIT => 'International Transit',
|
||||
self::STATUS_ARRIVED_INDIA => 'Arrived at India',
|
||||
self::STATUS_IMPORT_CUSTOM => 'Import Custom',
|
||||
self::STATUS_WAREHOUSE => 'Warehouse',
|
||||
self::STATUS_DOMESTIC_DISTRIBUTION => 'Domestic Distribution',
|
||||
self::STATUS_OUT_FOR_DELIVERY => 'Out for Delivery',
|
||||
self::STATUS_DELIVERED => 'Delivered',
|
||||
];
|
||||
}
|
||||
|
||||
public function statusLabel()
|
||||
{
|
||||
return self::statusOptions()[$this->status] ?? ucfirst($this->status);
|
||||
return self::statusOptions()[$this->status]
|
||||
?? ucfirst(str_replace('_', ' ', $this->status));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -89,11 +89,26 @@ class User extends Authenticatable implements JWTSubject
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
// App\Models\User.php
|
||||
|
||||
|
||||
public function invoiceInstallments()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
InvoiceInstallment::class,
|
||||
Invoice::class,
|
||||
'customer_id', // FK on invoices
|
||||
'invoice_id' // FK on installments
|
||||
);
|
||||
}
|
||||
|
||||
public function invoices()
|
||||
{
|
||||
return $this->hasMany(\App\Models\Invoice::class, 'customer_id', 'id');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'ttl' => (int) env('JWT_TTL', 15),
|
||||
'ttl' => (int) env('JWT_TTL', 1440),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -108,7 +108,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'refresh_ttl' => (int) env('JWT_REFRESH_TTL', 60),
|
||||
'refresh_ttl' => (int) env('JWT_REFRESH_TTL', 64800),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -33,6 +33,9 @@ class CreateInvoiceItemsTable extends Migration
|
||||
$table->string('shop_no')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unsignedBigInteger('container_id')->nullable()->after('invoice_id');
|
||||
$table->integer('container_row_index')->nullable()->after('container_id');
|
||||
|
||||
// FK
|
||||
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');
|
||||
@@ -49,4 +52,6 @@ class CreateInvoiceItemsTable extends Migration
|
||||
});
|
||||
Schema::dropIfExists('invoice_items');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('containers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('container_name');
|
||||
$table->string('container_number')->unique();
|
||||
$table->date('container_date');
|
||||
$table->string('excel_file')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('containers');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('loading_list_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('container_id')
|
||||
->constrained('containers')
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->string('mark')->nullable(); // MARK / ITEM NO
|
||||
$table->string('description')->nullable();
|
||||
$table->integer('ctn')->nullable();
|
||||
$table->integer('qty')->nullable();
|
||||
$table->integer('total_qty')->nullable();
|
||||
$table->string('unit')->nullable();
|
||||
$table->decimal('price', 15, 3)->nullable(); // SAHIL format साठी
|
||||
$table->decimal('cbm', 15, 5)->nullable();
|
||||
$table->decimal('total_cbm', 15, 5)->nullable();
|
||||
$table->decimal('kg', 15, 3)->nullable();
|
||||
$table->decimal('total_kg', 15, 3)->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('loading_list_items');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('container_rows', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('container_id')
|
||||
->constrained('containers')
|
||||
->onDelete('cascade');
|
||||
|
||||
// Excel मधल्या row क्रमांकासाठी (optional)
|
||||
$table->unsignedInteger('row_index')->nullable();
|
||||
|
||||
// या row चा full data: "heading text" => "cell value"
|
||||
$table->json('data');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('container_rows');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('containers', function (Blueprint $table) {
|
||||
$table->string('status', 21)
|
||||
->default('pending')
|
||||
->after('container_date');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('containers', function (Blueprint $table) {
|
||||
$table->dropColumn('status');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
// 1) order_id foreign key काढा
|
||||
$table->dropForeign(['order_id']);
|
||||
|
||||
// 2) order_id column काढा
|
||||
$table->dropColumn('order_id');
|
||||
|
||||
// 3) container_id add करा
|
||||
$table->unsignedBigInteger('container_id')->nullable()->after('id');
|
||||
|
||||
// 4) container_id FK
|
||||
$table->foreign('container_id')
|
||||
->references('id')
|
||||
->on('containers')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
// rollback: container_id काढून order_id परत add
|
||||
$table->dropForeign(['container_id']);
|
||||
$table->dropColumn('container_id');
|
||||
|
||||
$table->unsignedBigInteger('order_id')->index();
|
||||
$table->foreign('order_id')
|
||||
->references('id')
|
||||
->on('orders')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('invoice_charge_groups', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('invoice_id');
|
||||
$table->string('group_name')->nullable(); // उदा. "FREIGHT", "HANDLING"
|
||||
$table->enum('basis_type', ['ttl_qty', 'amount', 'ttl_cbm', 'ttl_kg']);
|
||||
$table->decimal('basis_value', 15, 3)->default(0); // auto calculate केलेला total basis
|
||||
$table->decimal('rate', 15, 3)->default(0); // per basis rate (helper)
|
||||
$table->decimal('total_charge', 15, 2); // admin नी manually टाकलेला total
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('invoice_id')
|
||||
->references('id')->on('invoices')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('invoice_charge_groups');
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('invoice_charge_group_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('group_id');
|
||||
$table->unsignedBigInteger('invoice_item_id');
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('group_id')
|
||||
->references('id')->on('invoice_charge_groups')
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->foreign('invoice_item_id')
|
||||
->references('id')->on('invoice_items')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('invoice_charge_group_items');
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('invoice_items', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('container_id')->nullable()->after('invoice_id');
|
||||
$table->integer('container_row_index')->nullable()->after('container_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('invoice_items', function (Blueprint $table) {
|
||||
$table->dropColumn(['container_id', 'container_row_index']);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
// column आधीच आहे का हे check करून, नसेल तरच add करायचा
|
||||
if (!Schema::hasColumn('invoices', 'due_date')) {
|
||||
$table->date('due_date')
|
||||
->nullable()
|
||||
->after('invoice_date');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('invoices', 'due_date')) {
|
||||
$table->dropColumn('due_date');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('containers', function (Blueprint $table) {
|
||||
$table->string('status', 50)->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('containers', function (Blueprint $table) {
|
||||
$table->string('status', 20)->change();
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('invoice_charge_groups', function (Blueprint $table) {
|
||||
$table->string('tax_type')->nullable()->after('total_charge');
|
||||
$table->decimal('gst_percent', 5, 2)->default(0)->after('tax_type');
|
||||
$table->decimal('total_with_gst', 15, 2)->default(0)->after('gst_percent');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('invoice_charge_groups', function (Blueprint $table) {
|
||||
$table->dropColumn(['tax_type', 'gst_percent', 'total_with_gst']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddChargeColumnsToInvoicesTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('invoices', 'charge_groups_total')) {
|
||||
$table->decimal('charge_groups_total', 15, 2)
|
||||
->nullable()
|
||||
->after('final_amount_with_gst');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('invoices', 'grand_total_with_charges')) {
|
||||
$table->decimal('grand_total_with_charges', 15, 2)
|
||||
->nullable()
|
||||
->after('charge_groups_total');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('invoices', 'charge_groups_total')) {
|
||||
$table->dropColumn('charge_groups_total');
|
||||
}
|
||||
if (Schema::hasColumn('invoices', 'grand_total_with_charges')) {
|
||||
$table->dropColumn('grand_total_with_charges');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
DB::statement("
|
||||
ALTER TABLE `invoices`
|
||||
MODIFY `status` ENUM('pending','paying','paid','overdue')
|
||||
NOT NULL DEFAULT 'pending'
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::statement("
|
||||
ALTER TABLE `invoices`
|
||||
MODIFY `status` ENUM('pending','paid','overdue')
|
||||
NOT NULL DEFAULT 'pending'
|
||||
");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('invoice_items', function (Blueprint $table) {
|
||||
$table->string('mark_no')->nullable(); // after() काहीही नको
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('invoice_items', function (Blueprint $table) {
|
||||
$table->dropColumn('mark_no');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
@@ -25,6 +25,12 @@ class PermissionSeeder extends Seeder
|
||||
// EXTRA (ORDERS)
|
||||
'orders.view', // you added this separately
|
||||
|
||||
// CONTAINER
|
||||
'container.view',
|
||||
'container.create',
|
||||
'container.update',
|
||||
'container.delete',
|
||||
|
||||
// SHIPMENT
|
||||
'shipment.view',
|
||||
'shipment.create',
|
||||
|
||||
BIN
public/images/kentlogo1.png
Normal file
BIN
public/images/kentlogo1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
BIN
public/invoices/invoice-INV-2025-000016.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000016.pdf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000031.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000031.pdf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000034.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000034.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000044.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000044.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000046.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000046.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000048.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000048.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000051.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000051.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000007.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000007.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000012.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000012.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000013.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000013.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000014.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000014.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000015.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000015.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000110.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000110.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000116.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000116.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000117.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000117.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000134.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000134.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000183.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000183.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000184.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000184.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000185.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000185.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000202.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000202.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000205.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000205.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000206.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000206.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000209.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000209.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000213.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000213.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000214.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000214.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000217.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000217.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000218.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000218.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000220.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000220.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000221.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000221.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000222.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000222.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000225.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000225.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000253.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000253.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000256.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000256.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000257.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000257.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000258.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000258.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000259.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000259.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000260.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000260.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000261.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000261.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000262.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000262.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000263.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000263.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000264.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000264.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000265.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000265.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,102 @@
|
||||
<div class="container-fluid py-2">
|
||||
|
||||
{{-- Top info cards (container / date / status) --}}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Container Name</small>
|
||||
<div class="fw-semibold">{{ $container->container_name ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Container No</small>
|
||||
<div class="fw-semibold">{{ $container->container_number ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Container Date</small>
|
||||
<div class="fw-semibold">
|
||||
{{ $container->container_date ? \Carbon\Carbon::parse($container->container_date)->format('d-m-Y') : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Status</small>
|
||||
<div class="fw-semibold text-capitalize">{{ $container->status ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Totals (CTN / Qty / CBM / KG) --}}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Total CTN</small>
|
||||
<div class="fw-semibold">{{ $summary['total_ctn'] ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Total Qty</small>
|
||||
<div class="fw-semibold">{{ $summary['total_qty'] ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Total CBM</small>
|
||||
<div class="fw-semibold">{{ $summary['total_cbm'] ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Total KG</small>
|
||||
<div class="fw-semibold">{{ $summary['total_kg'] ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Excel rows – same headings as container_show --}}
|
||||
@php
|
||||
$allHeadings = [];
|
||||
foreach ($container->rows as $row) {
|
||||
if (is_array($row->data)) {
|
||||
$allHeadings = array_unique(array_merge($allHeadings, array_keys($row->data)));
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="table-responsive" style="max-height: 500px; border-radius: 8px;">
|
||||
<table class="table table-sm table-bordered align-middle">
|
||||
<thead class="table-warning">
|
||||
<tr>
|
||||
<th style="width: 40px;">#</th>
|
||||
@foreach($allHeadings as $heading)
|
||||
<th>{{ $heading }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($container->rows as $index => $row)
|
||||
<tr>
|
||||
<td>{{ $index + 1 }}</td>
|
||||
@foreach($allHeadings as $heading)
|
||||
@php
|
||||
$val = is_array($row->data) ? ($row->data[$heading] ?? '') : '';
|
||||
@endphp
|
||||
<td>{{ $val }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ count($allHeadings) + 1 }}" class="text-center text-muted py-3">
|
||||
No Excel rows for this container.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,10 +10,10 @@
|
||||
--danger-gradient: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%);
|
||||
--warning-gradient: linear-gradient(135deg, #f7971e 0%, #ffd200 100%);
|
||||
--info-gradient: linear-gradient(135deg, #56ccf2 0%, #2f80ed 100%);
|
||||
--card-shadow: 0 8px 25px rgba(0,0,0,0.08);
|
||||
--card-shadow-hover: 0 15px 35px rgba(0,0,0,0.12);
|
||||
--border-radius: 12px;
|
||||
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--card-shadow: 0 3px 10px rgba(0,0,0,0.05);
|
||||
--card-shadow-hover: 0 6px 16px rgba(0,0,0,0.10);
|
||||
--border-radius: 8px;
|
||||
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -23,24 +23,24 @@
|
||||
.chat-dashboard {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 1.5rem;
|
||||
padding: 0.75rem;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: clamp(2rem, 4vw, 3rem);
|
||||
font-size: clamp(1.4rem, 2.5vw, 2rem);
|
||||
font-weight: 800;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin: 0 0 0.75rem 0;
|
||||
margin: 0 0 0.4rem 0;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -48,143 +48,106 @@
|
||||
.dashboard-title::before {
|
||||
content: '💬';
|
||||
position: absolute;
|
||||
left: -2.5rem;
|
||||
left: -1.6rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 2rem;
|
||||
font-size: 1.3rem;
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% { transform: translateY(-50%) translateY(0); }
|
||||
40% { transform: translateY(-50%) translateY(-8px); }
|
||||
60% { transform: translateY(-50%) translateY(-4px); }
|
||||
40% { transform: translateY(-50%) translateY(-5px); }
|
||||
60% { transform: translateY(-50%) translateY(-2px); }
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
color: #64748b;
|
||||
font-size: 1rem;
|
||||
max-width: 500px;
|
||||
font-size: 0.8rem;
|
||||
max-width: 380px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.5;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(15px);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1.25rem 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: var(--primary-gradient);
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--card-shadow-hover);
|
||||
}
|
||||
|
||||
.stat-card:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
border-radius: 12px;
|
||||
/* 🔔 GLOBAL NEW MESSAGE COUNTER */
|
||||
.global-notify {
|
||||
margin: 0 auto 0.75rem auto;
|
||||
max-width: 320px;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(15,23,42,0.03);
|
||||
border: 1px dashed #cbd5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-total { background: var(--primary-gradient); color: white; }
|
||||
.stat-open { background: var(--success-gradient); color: white; }
|
||||
.stat-closed{ background: var(--danger-gradient); color: white; }
|
||||
|
||||
.stat-content h3 {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
margin: 0 0 0.125rem 0;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.78rem;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.stat-content p {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
.global-notify-badge {
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
min-width: 18px;
|
||||
padding: 0 0.35rem;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 0 0 2px rgba(254, 226, 226, 0.8);
|
||||
}
|
||||
|
||||
.global-notify.d-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tickets-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(15px);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1.5rem;
|
||||
padding: 0.75rem 0.9rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
/* max-height / overflow काढले, जेणेकरून बाहेरचा page scroll वापरला जाईल */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tickets-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 0.6rem;
|
||||
padding-bottom: 0.45rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.tickets-title {
|
||||
font-size: 1.4rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.tickets-count {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 16px;
|
||||
font-size: 0.8rem;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ========= COMPACT TICKET CARD ========= */
|
||||
.ticket-item {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 0.6rem;
|
||||
border: 1px solid #f1f5f9;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.6rem;
|
||||
margin-bottom: 0.3rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -196,16 +159,16 @@
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
width: 2px;
|
||||
background: var(--primary-gradient);
|
||||
transform: scaleY(0);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.ticket-item:hover {
|
||||
transform: translateY(-2px);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--card-shadow-hover);
|
||||
border-color: rgba(102, 126, 234, 0.15);
|
||||
border-color: rgba(102, 126, 234, 0.35);
|
||||
}
|
||||
|
||||
.ticket-item:hover::before {
|
||||
@@ -215,23 +178,23 @@
|
||||
.ticket-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 0.5rem;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.ticket-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 6px;
|
||||
background: var(--info-gradient);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 6px 16px rgba(86, 204, 242, 0.25);
|
||||
box-shadow: 0 3px 8px rgba(86, 204, 242, 0.25);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -244,7 +207,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
transition: left 0.5s;
|
||||
transition: left 0.4s;
|
||||
}
|
||||
|
||||
.ticket-item:hover .ticket-avatar::after {
|
||||
@@ -257,27 +220,27 @@
|
||||
}
|
||||
|
||||
.ticket-name {
|
||||
font-size: 0.95rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 0 0 0.15rem 0;
|
||||
margin: 0 0 0.08rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.unread-count {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 3px 10px rgba(239, 68, 68, 0.4);
|
||||
box-shadow: 0 2px 6px rgba(239, 68, 68, 0.4);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@@ -288,25 +251,27 @@
|
||||
|
||||
.ticket-preview {
|
||||
color: #64748b;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.ticket-time {
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.25;
|
||||
margin-bottom: 0.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
max-height: 1.8em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-time {
|
||||
font-size: 0.65rem;
|
||||
color: #94a3b8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
.ticket-time svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -314,18 +279,18 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 0.6rem;
|
||||
border-top: 1px dashed #f1f5f9;
|
||||
margin-top: 0.4rem;
|
||||
padding-top: 0.3rem;
|
||||
border-top: 1px dashed #e5e7eb;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.7rem;
|
||||
border-radius: 50px;
|
||||
font-size: 0.7rem;
|
||||
padding: 0.14rem 0.45rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.status-open { background: var(--success-gradient); color: white; }
|
||||
@@ -335,86 +300,87 @@
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.45rem 0.9rem;
|
||||
border-radius: 25px;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
display: flex;
|
||||
font-size: 0.7rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
gap: 0.25rem;
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.25);
|
||||
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.25);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.35);
|
||||
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.35);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat-btn::after {
|
||||
content: '→';
|
||||
transition: margin-left 0.3s ease;
|
||||
transition: margin-left 0.25s ease;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.chat-btn:hover::after {
|
||||
margin-left: 0.4rem;
|
||||
margin-left: 0.18rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 2.5rem 1.5rem;
|
||||
padding: 1.6rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: var(--border-radius);
|
||||
border: 2px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 2.8rem;
|
||||
margin-bottom: 0.9rem;
|
||||
display: block;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-15px); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 1.4rem;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.empty-subtitle {
|
||||
color: #64748b;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.5;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.ticket-id {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
background: rgba(102, 126, 234, 0.08);
|
||||
color: #667eea;
|
||||
padding: 0.2rem 0.75rem;
|
||||
border-radius: 16px;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.12rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
.new-message-dot {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
top: 0.45rem;
|
||||
right: 0.5rem;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: #ef4444;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
@@ -425,53 +391,52 @@
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(1.15); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chat-dashboard {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.ticket-header {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.ticket-footer {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.chat-btn {
|
||||
justify-content: center;
|
||||
}
|
||||
50% { opacity: 0.5; transform: scale(1.1); }
|
||||
}
|
||||
|
||||
/* इथे आता inner scroll नाही */
|
||||
.tickets-list {
|
||||
max-height: 55vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 0.4rem;
|
||||
/* flex: 1; काढला, overflow काढला, parent + body scroll वापरेल */
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar-thumb {
|
||||
background: var(--primary-gradient);
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chat-dashboard {
|
||||
padding: 0.6rem;
|
||||
}
|
||||
|
||||
.tickets-container {
|
||||
/* max-height काढलेले, mobile वरही outer scroll */
|
||||
}
|
||||
|
||||
.ticket-header {
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.ticket-footer {
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.chat-btn {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -484,29 +449,10 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon stat-total">💬</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ $tickets->count() }}</h3>
|
||||
<p>Total Conversations</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon stat-open">📈</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ $tickets->where('status', 'open')->count() }}</h3>
|
||||
<p>Active Tickets</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon stat-closed">✅</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ $tickets->where('status', 'closed')->count() }}</h3>
|
||||
<p>Resolved Tickets</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 🔔 GLOBAL NEW MESSAGES NOTIFICATION -->
|
||||
<div id="globalNewMessageBox" class="global-notify d-none">
|
||||
<span>New messages:</span>
|
||||
<span id="globalNewMessageCount" class="global-notify-badge">0</span>
|
||||
</div>
|
||||
|
||||
<!-- Tickets Container -->
|
||||
@@ -552,15 +498,15 @@
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
||||
@php
|
||||
$lastMsg = $ticket->messages()->latest()->first();
|
||||
@endphp
|
||||
|
||||
|
||||
<div class="ticket-preview">
|
||||
@if($lastMsg)
|
||||
@if($lastMsg->message)
|
||||
{{ Str::limit($lastMsg->message, 55) }}
|
||||
{{ Str::limit($lastMsg->message, 45) }}
|
||||
@elseif(Str::startsWith($lastMsg->file_type, 'image'))
|
||||
📷 Photo shared
|
||||
@else
|
||||
@@ -581,7 +527,6 @@
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="ticket-id">#{{ $ticket->id }}</div>
|
||||
</div>
|
||||
|
||||
<div class="ticket-footer">
|
||||
@@ -626,21 +571,51 @@ function waitForEcho(callback, retries = 40) {
|
||||
waitForEcho(() => {
|
||||
console.log('[ADMIN LIST] Listening for new messages...');
|
||||
|
||||
const globalBox = document.getElementById('globalNewMessageBox');
|
||||
const globalCount = document.getElementById('globalNewMessageCount');
|
||||
let totalNewMessages = 0;
|
||||
|
||||
window.Echo.private('admin.chat')
|
||||
.listen('.NewChatMessage', (event) => {
|
||||
|
||||
// only USER → ADMIN messages
|
||||
if (event.sender_type !== 'App\\Models\\User') return;
|
||||
|
||||
const badge = document.getElementById(`badge-${event.ticket_id}`);
|
||||
if (!badge) return;
|
||||
const ticketId = event.ticket_id;
|
||||
|
||||
let count = parseInt(badge.innerText || 0);
|
||||
badge.innerText = count + 1;
|
||||
badge.classList.remove('d-none');
|
||||
// 1) UPDATE PER-TICKET BADGE
|
||||
const badge = document.getElementById(`badge-${ticketId}`);
|
||||
if (badge) {
|
||||
let count = parseInt(badge.innerText || 0);
|
||||
badge.innerText = count + 1;
|
||||
badge.classList.remove('d-none');
|
||||
}
|
||||
|
||||
console.log('[ADMIN LIST] Badge updated for ticket', event.ticket_id);
|
||||
// 2) UPDATE GLOBAL NEW MESSAGE COUNTER
|
||||
totalNewMessages++;
|
||||
if (globalBox && globalCount) {
|
||||
globalBox.classList.remove('d-none');
|
||||
globalCount.innerText = totalNewMessages;
|
||||
}
|
||||
|
||||
// 3) त्या ticket ला यादीत सर्वात वर आणा आणि स्क्रोल करा
|
||||
const listContainer = document.querySelector('.tickets-list');
|
||||
const ticketEl = document.querySelector(`.ticket-item[data-ticket-id="${ticketId}"]`);
|
||||
|
||||
if (listContainer && ticketEl) {
|
||||
if (ticketEl.previousElementSibling) {
|
||||
listContainer.insertBefore(ticketEl, listContainer.firstElementChild);
|
||||
}
|
||||
|
||||
const topOffset = ticketEl.offsetTop;
|
||||
window.scrollTo({
|
||||
top: listContainer.offsetTop + topOffset - 20,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[ADMIN LIST] Badge/global counter updated for ticket', ticketId);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@endsection
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
/* ... तुझा existing complete CSS जसाच्या तसा ठेव ... */
|
||||
:root {
|
||||
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--primary-soft: rgba(102, 126, 234, 0.12);
|
||||
@@ -643,6 +644,32 @@ body {
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
.header-notify {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
color: #cbd5f5;
|
||||
}
|
||||
|
||||
.header-notify-badge {
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 0 0 2px rgba(15,23,42,0.8);
|
||||
}
|
||||
|
||||
.header-notify.d-none {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="chat-container">
|
||||
@@ -662,6 +689,12 @@ body {
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
{{-- per‑ticket new message notification --}}
|
||||
<div id="ticketNotifyBox" class="header-notify d-none">
|
||||
New messages:
|
||||
<span id="ticketNotifyCount" class="header-notify-badge">0</span>
|
||||
</div>
|
||||
|
||||
<span class="status-badge {{ $ticket->status === 'open' ? 'open' : 'closed' }}">
|
||||
{{ ucfirst($ticket->status) }}
|
||||
</span>
|
||||
@@ -692,8 +725,7 @@ body {
|
||||
$isPdf = Str::endsWith($msg->file_path, '.pdf');
|
||||
$isDocument = in_array(Str::lower(Str::afterLast($msg->file_path, '.')), ['doc', 'docx', 'txt']);
|
||||
$fileName = basename($msg->file_path);
|
||||
|
||||
// Get file size from storage if possible
|
||||
|
||||
$fileSize = 'N/A';
|
||||
try {
|
||||
$fullPath = storage_path('app/public/' . $msg->file_path);
|
||||
@@ -710,8 +742,7 @@ body {
|
||||
} catch (Exception $e) {
|
||||
$fileSize = 'Unknown';
|
||||
}
|
||||
|
||||
// Determine file class
|
||||
|
||||
if ($isImage) {
|
||||
$fileClass = 'image-file';
|
||||
$fileIcon = '🖼️';
|
||||
@@ -744,9 +775,9 @@ body {
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
@endif
|
||||
|
||||
<a href="{{ asset('storage/'.$msg->file_path) }}"
|
||||
target="_blank"
|
||||
|
||||
<a href="{{ asset('storage/'.$msg->file_path) }}"
|
||||
target="_blank"
|
||||
class="file-item {{ $fileClass }}"
|
||||
title="{{ $fileName }}">
|
||||
<div class="file-icon">
|
||||
@@ -758,7 +789,7 @@ body {
|
||||
</div>
|
||||
<div class="file-download-btn">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
@@ -789,10 +820,11 @@ body {
|
||||
<div class="file-name-display" id="fileNameDisplay"></div>
|
||||
<button class="clear-file-btn" id="clearFileBtn" title="Remove file">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
|
||||
<div class="file-upload-btn">
|
||||
<input type="file" id="fileInput">
|
||||
<label for="fileInput" title="Attach file">
|
||||
@@ -823,8 +855,6 @@ body {
|
||||
@section('scripts')
|
||||
<script>
|
||||
const CURRENT_ADMIN_ID = {{ auth('admin')->id() }};
|
||||
|
||||
// ✅ Make current admin ID available to JS
|
||||
console.log("CHAT WINDOW: script loaded");
|
||||
|
||||
// -------------------------------
|
||||
@@ -859,13 +889,12 @@ const fileInput = document.getElementById('fileInput');
|
||||
const fileNameDisplay = document.getElementById('fileNameDisplay');
|
||||
const clearFileBtn = document.getElementById('clearFileBtn');
|
||||
|
||||
fileInput.addEventListener('change', function(e) {
|
||||
fileInput.addEventListener('change', function() {
|
||||
if (this.files.length > 0) {
|
||||
const file = this.files[0];
|
||||
const fileName = file.name;
|
||||
const fileSize = formatFileSize(file.size);
|
||||
|
||||
fileNameDisplay.textContent = `${fileName} (${fileSize})`;
|
||||
const name = file.name;
|
||||
const size = formatFileSize(file.size);
|
||||
fileNameDisplay.textContent = `${name} (${size})`;
|
||||
fileNameDisplay.style.display = 'block';
|
||||
clearFileBtn.style.display = 'block';
|
||||
}
|
||||
@@ -886,40 +915,26 @@ function formatFileSize(bytes) {
|
||||
}
|
||||
|
||||
function getFileIcon(fileType, fileName) {
|
||||
if (fileType.startsWith('image/')) {
|
||||
return '🖼️';
|
||||
} else if (fileType.startsWith('video/')) {
|
||||
return '🎬';
|
||||
} else if (fileName.endsWith('.pdf')) {
|
||||
return '📄';
|
||||
} else if (fileName.match(/\.(doc|docx|txt)$/i)) {
|
||||
return '📝';
|
||||
} else {
|
||||
return '📎';
|
||||
}
|
||||
if (fileType.startsWith('image/')) return '🖼️';
|
||||
if (fileType.startsWith('video/')) return '🎬';
|
||||
if (fileName.endsWith('.pdf')) return '📄';
|
||||
if (fileName.match(/\.(doc|docx|txt)$/i)) return '📝';
|
||||
return '📎';
|
||||
}
|
||||
|
||||
function getFileClass(fileType, fileName) {
|
||||
if (fileType.startsWith('image/')) {
|
||||
return 'image-file';
|
||||
} else if (fileType.startsWith('video/')) {
|
||||
return 'video-file';
|
||||
} else if (fileName.endsWith('.pdf')) {
|
||||
return 'pdf-file';
|
||||
} else if (fileName.match(/\.(doc|docx|txt)$/i)) {
|
||||
return 'document-file';
|
||||
} else {
|
||||
return 'other-file';
|
||||
}
|
||||
if (fileType.startsWith('image/')) return 'image-file';
|
||||
if (fileType.startsWith('video/')) return 'video-file';
|
||||
if (fileName.endsWith('.pdf')) return 'pdf-file';
|
||||
if (fileName.match(/\.(doc|docx|txt)$/i)) return 'document-file';
|
||||
return 'other-file';
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// SEND MESSAGE
|
||||
// -------------------------------
|
||||
document.getElementById("sendBtn").addEventListener("click", function () {
|
||||
console.log("[SEND] Attempting to send message...");
|
||||
|
||||
let msg = document.getElementById("messageInput").value;
|
||||
let msg = document.getElementById("messageInput").value;
|
||||
let file = document.getElementById("fileInput").files[0];
|
||||
|
||||
if (!msg.trim() && !file) {
|
||||
@@ -937,8 +952,7 @@ document.getElementById("sendBtn").addEventListener("click", function () {
|
||||
body: formData
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then((response) => {
|
||||
console.log("[SEND] Message sent:", response);
|
||||
.then(() => {
|
||||
document.getElementById("messageInput").value = "";
|
||||
document.getElementById("fileInput").value = "";
|
||||
fileNameDisplay.style.display = 'none';
|
||||
@@ -957,15 +971,18 @@ document.getElementById("messageInput").addEventListener("keypress", function(e)
|
||||
|
||||
// -------------------------------
|
||||
// LISTEN FOR REALTIME MESSAGE
|
||||
// ----------------------------
|
||||
// -------------------------------
|
||||
waitForEcho(() => {
|
||||
const ticketId = "{{ $ticket->id }}";
|
||||
|
||||
const notifyBox = document.getElementById('ticketNotifyBox');
|
||||
const notifyCount = document.getElementById('ticketNotifyCount');
|
||||
let ticketNewCount = 0;
|
||||
|
||||
console.log("[ECHO] Subscribing to PRIVATE channel:", `ticket.${ticketId}`);
|
||||
|
||||
window.Echo.private(`ticket.${ticketId}`)
|
||||
.listen(".NewChatMessage", (event) => {
|
||||
|
||||
console.log("%c[REALTIME RECEIVED]", "color: blue; font-weight: bold;", event);
|
||||
|
||||
const msg = event;
|
||||
@@ -973,67 +990,69 @@ waitForEcho(() => {
|
||||
msg.sender_type === 'App\\Models\\Admin' &&
|
||||
msg.sender_id === CURRENT_ADMIN_ID;
|
||||
|
||||
// जर message admin ने पाठवला असेल तर त्या साठी notification वाढवायची गरज नाही
|
||||
if (!isMine) {
|
||||
ticketNewCount++;
|
||||
if (notifyBox && notifyCount) {
|
||||
notifyBox.classList.remove('d-none');
|
||||
notifyCount.textContent = ticketNewCount;
|
||||
}
|
||||
}
|
||||
|
||||
let html = `
|
||||
<div class="message ${isMine ? 'admin' : 'user'}">
|
||||
${msg.message ? `<div>${msg.message}</div>` : ''}
|
||||
`;
|
||||
|
||||
if (msg.file_path) {
|
||||
const fileUrl = `/storage/${msg.file_path}`;
|
||||
const fileUrl = `/storage/${msg.file_path}`;
|
||||
const fileName = msg.file_path.split('/').pop();
|
||||
const fileSize = msg.file_size ? formatFileSize(msg.file_size) : 'Unknown';
|
||||
const fileIcon = getFileIcon(msg.file_type || '', fileName);
|
||||
const fileClass = getFileClass(msg.file_type || '', fileName);
|
||||
|
||||
// Check if it's image or video for preview
|
||||
const isImage = (msg.file_type || '').startsWith('image');
|
||||
const isVideo = (msg.file_type || '').startsWith('video');
|
||||
const fileClass= getFileClass(msg.file_type || '', fileName);
|
||||
const isImage = (msg.file_type || '').startsWith('image');
|
||||
const isVideo = (msg.file_type || '').startsWith('video');
|
||||
|
||||
html += `<div class="file-preview">`;
|
||||
|
||||
if (isImage) {
|
||||
html += `
|
||||
<img src="${fileUrl}"
|
||||
class="rounded mt-2"
|
||||
<img src="${fileUrl}"
|
||||
class="rounded mt-2"
|
||||
style="max-width:210px;"
|
||||
alt="${fileName}">
|
||||
`;
|
||||
} else if (isVideo) {
|
||||
html += `
|
||||
<video src="${fileUrl}"
|
||||
controls
|
||||
class="mt-2 rounded"
|
||||
<video src="${fileUrl}"
|
||||
controls
|
||||
class="mt-2 rounded"
|
||||
style="max-width:200px;">
|
||||
</video>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<a href="${fileUrl}"
|
||||
target="_blank"
|
||||
<a href="${fileUrl}"
|
||||
target="_blank"
|
||||
class="file-item ${fileClass}"
|
||||
title="${fileName}">
|
||||
<div class="file-icon">
|
||||
${fileIcon}
|
||||
</div>
|
||||
<div class="file-icon">${fileIcon}</div>
|
||||
<div class="file-info">
|
||||
<div class="file-name">${fileName}</div>
|
||||
<div class="file-size">${fileSize}</div>
|
||||
</div>
|
||||
<div class="file-download-btn">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<small class="mt-2">Just now</small>
|
||||
</div>
|
||||
`;
|
||||
html += `<small class="mt-2">Just now</small></div>`;
|
||||
|
||||
document
|
||||
.querySelector("#chatBox .chat-box-inner")
|
||||
@@ -1043,4 +1062,4 @@ waitForEcho(() => {
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@endsection
|
||||
|
||||
855
resources/views/admin/container.blade.php
Normal file
855
resources/views/admin/container.blade.php
Normal file
@@ -0,0 +1,855 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Containers')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #4c6fff;
|
||||
--primary-gradient: linear-gradient(135deg, #4c6fff, #8e54e9);
|
||||
--success-color: #10b981;
|
||||
--warning-color: #f59e0b;
|
||||
--danger-color: #ef4444;
|
||||
--info-color: #3b82f6;
|
||||
--light-bg: #f8fafc;
|
||||
--dark-text: #1e293b;
|
||||
--gray-text: #64748b;
|
||||
--border-color: #e2e8f0;
|
||||
--shadow-sm: 0 1px 3px rgba(0,0,0,0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1);
|
||||
--shadow-lg: 0 10px 25px -5px rgba(0,0,0,0.1);
|
||||
--radius-lg: 16px;
|
||||
--radius-md: 12px;
|
||||
--radius-sm: 8px;
|
||||
}
|
||||
|
||||
.containers-wrapper {
|
||||
min-height: calc(100vh - 180px);
|
||||
padding: 20px 15px;
|
||||
background: linear-gradient(135deg, #f6f9ff 0%, #f0f4ff 100%);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
color: var(--gray-text);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.add-container-btn {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 28px;
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 12px rgba(76, 111, 255, 0.3);
|
||||
}
|
||||
|
||||
.add-container-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(76, 111, 255, 0.4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.add-container-btn i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: 1px solid rgba(255,255,255,0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.filter-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--dark-text);
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-title i { color: var(--primary-color); }
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-group { position: relative; }
|
||||
|
||||
.filter-group label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-text);
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.filter-input, .filter-select, .filter-date {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 14px;
|
||||
color: var(--dark-text);
|
||||
background: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.filter-input:focus, .filter-select:focus, .filter-date:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(76, 111, 255, 0.1);
|
||||
}
|
||||
|
||||
.filter-input::placeholder { color: #94a3b8; }
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.apply-btn {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 46px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.apply-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 20px rgba(76, 111, 255, 0.3);
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: white;
|
||||
color: var(--gray-text);
|
||||
border: 2px solid var(--border-color);
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 46px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.main-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-lg);
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid rgba(255,255,255,0.9);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 24px;
|
||||
background: linear-gradient(135deg, #4c6fff, #8e54e9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.card-header h2 i { color: white; }
|
||||
|
||||
.stats-badge {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
margin-left: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.container-item {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.container-item:hover {
|
||||
background: #f8fafc;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.container-item:last-child { border-bottom: none; }
|
||||
|
||||
.container-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.container-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.container-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-gradient);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4px 12px rgba(76, 111, 255, 0.3);
|
||||
}
|
||||
|
||||
.container-details h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--dark-text);
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.container-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--gray-text);
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.meta-item i {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* STATUS DROPDOWN (badge look) */
|
||||
.status-dropdown {
|
||||
position: relative;
|
||||
min-width: 190px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status-dropdown-toggle {
|
||||
padding: 8px 16px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: #ffffff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--dark-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.status-dropdown-toggle span { white-space: nowrap; }
|
||||
|
||||
.status-dropdown-menu {
|
||||
position: absolute;
|
||||
top: -230%;
|
||||
right: 0;
|
||||
z-index: 30;
|
||||
background: #ffffff;
|
||||
border-radius: 14px;
|
||||
padding: 8px 0;
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: 1px solid var(--border-color);
|
||||
width: 220px;
|
||||
max-height: 340px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status-dropdown-menu.open { display: block; }
|
||||
|
||||
.status-option {
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
color: var(--dark-text);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: background 0.15s;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.status-option:hover { background: #eef2ff; }
|
||||
|
||||
.status-option .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
.status-option.active .dot {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
/* COLOR MAPPING per status – dropdown tint + main toggle text color */
|
||||
.status-option.status-container-ready { background: #eff6ff; color: #1d4ed8; }
|
||||
.status-option.status-export-custom { background: #fff7ed; color: #b45309; }
|
||||
.status-option.status-international-transit { background: #f5f3ff; color: #4c1d95; }
|
||||
.status-option.status-arrived-at-india { background: #ecfdf5; color: #15803d; }
|
||||
.status-option.status-import-custom { background: #fffbeb; color: #92400e; }
|
||||
.status-option.status-warehouse { background: #f4f4f5; color: #374151; }
|
||||
.status-option.status-domestic-distribution { background: #faf5ff; color: #6d28d9; }
|
||||
.status-option.status-out-for-delivery { background: #eff6ff; color: #1d4ed8; }
|
||||
.status-option.status-delivered { background: #ecfdf5; color: #15803d; }
|
||||
|
||||
.status-dropdown-toggle.status-container-ready { background: #eff6ff; color: #1d4ed8; }
|
||||
.status-dropdown-toggle.status-export-custom { background: #fff7ed; color: #b45309; }
|
||||
.status-dropdown-toggle.status-international-transit { background: #f5f3ff; color: #4c1d95; }
|
||||
.status-dropdown-toggle.status-arrived-at-india { background: #ecfdf5; color: #15803d; }
|
||||
.status-dropdown-toggle.status-import-custom { background: #fffbeb; color: #92400e; }
|
||||
.status-dropdown-toggle.status-warehouse { background: #f4f4f5; color: #374151; }
|
||||
.status-dropdown-toggle.status-domestic-distribution { background: #faf5ff; color: #6d28d9; }
|
||||
.status-dropdown-toggle.status-out-for-delivery { background: #eff6ff; color: #1d4ed8; }
|
||||
.status-dropdown-toggle.status-delivered { background: #ecfdf5; color: #15803d; }
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
background: #e0f2fe;
|
||||
color: #0369a1;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
background: #0ea5e9;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.update-form { position: relative; }
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.no-results-icon {
|
||||
font-size: 64px;
|
||||
color: var(--border-color);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.no-results h4 {
|
||||
font-size: 18px;
|
||||
color: var(--gray-text);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.no-results p {
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
box-shadow: var(--shadow-md);
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.success-message i { font-size: 20px; }
|
||||
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.totals-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
border-radius: var(--radius-md);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.total-card {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.total-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.total-value {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.total-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-text);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
.add-container-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.filter-grid { grid-template-columns: 1fr; }
|
||||
.container-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.update-form { width: 100%; }
|
||||
.status-dropdown { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="containers-wrapper">
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>Container Management</h1>
|
||||
<div class="header-subtitle">
|
||||
Manage all containers, track status, and view entries in real-time
|
||||
</div>
|
||||
</div>
|
||||
@can('container.create')
|
||||
<a href="{{ route('containers.create') }}" class="add-container-btn">
|
||||
<i class="fas fa-plus-circle"></i>
|
||||
Add New Container
|
||||
</a>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="success-message">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>{{ session('success') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="filter-card">
|
||||
<div class="filter-title">
|
||||
<i class="fas fa-filter"></i>
|
||||
Filter Containers
|
||||
</div>
|
||||
<form method="GET" class="filter-grid">
|
||||
<div class="filter-group">
|
||||
<label><i class="fas fa-search"></i> Search</label>
|
||||
<input type="text" name="search" class="filter-input"
|
||||
placeholder="Search by container name or number..."
|
||||
value="{{ request('search') }}">
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label><i class="fas fa-tag"></i> Status</label>
|
||||
<select name="status" class="filter-select">
|
||||
<option value="">All Status</option>
|
||||
<option value="container-ready" {{ request('status') == 'container-ready' ? 'selected' : '' }}>Container Ready</option>
|
||||
<option value="export-custom" {{ request('status') == 'export-custom' ? 'selected' : '' }}>Export Custom</option>
|
||||
<option value="international-transit" {{ request('status') == 'international-transit' ? 'selected' : '' }}>International Transit</option>
|
||||
<option value="arrived-at-india" {{ request('status') == 'arrived-at-india' ? 'selected' : '' }}>Arrived at India</option>
|
||||
<option value="import-custom" {{ request('status') == 'import-custom' ? 'selected' : '' }}>Import Custom</option>
|
||||
<option value="warehouse" {{ request('status') == 'warehouse' ? 'selected' : '' }}>Warehouse</option>
|
||||
<option value="domestic-distribution" {{ request('status') == 'domestic-distribution' ? 'selected' : '' }}>Domestic Distribution</option>
|
||||
<option value="out-for-delivery" {{ request('status') == 'out-for-delivery' ? 'selected' : '' }}>Out for Delivery</option>
|
||||
<option value="delivered" {{ request('status') == 'delivered' ? 'selected' : '' }}>Delivered</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label><i class="fas fa-calendar"></i> Date</label>
|
||||
<input type="date" name="date" class="filter-date" value="{{ request('date') }}">
|
||||
</div>
|
||||
|
||||
<div class="filter-actions">
|
||||
<button type="submit" class="apply-btn">
|
||||
<i class="fas fa-search"></i> Apply Filters
|
||||
</button>
|
||||
<a href="{{ route('containers.index') }}" class="reset-btn">
|
||||
<i class="fas fa-redo"></i> Reset
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="main-card">
|
||||
<div class="card-header">
|
||||
<h2>
|
||||
<i class="fas fa-boxes"></i>
|
||||
Containers List
|
||||
<span class="stats-badge">{{ $containers->count() }} containers</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@if($containers->isEmpty())
|
||||
<div class="no-results">
|
||||
<div class="no-results-icon">
|
||||
<i class="fas fa-box-open"></i>
|
||||
</div>
|
||||
<h4>No containers found</h4>
|
||||
<p>Get started by creating your first container</p>
|
||||
</div>
|
||||
@else
|
||||
@php
|
||||
$labels = [
|
||||
'container-ready' => 'Container Ready',
|
||||
'export-custom' => 'Export Custom',
|
||||
'international-transit' => 'International Transit',
|
||||
'arrived-at-india' => 'Arrived at India',
|
||||
'import-custom' => 'Import Custom',
|
||||
'warehouse' => 'Warehouse',
|
||||
'domestic-distribution' => 'Domestic Distribution',
|
||||
'out-for-delivery' => 'Out for Delivery',
|
||||
'delivered' => 'Delivered',
|
||||
];
|
||||
@endphp
|
||||
|
||||
@foreach($containers as $container)
|
||||
@php
|
||||
$status = $container->status ?? 'container-ready';
|
||||
$statusLabel = $labels[$status] ?? ucfirst(str_replace('-', ' ', $status));
|
||||
@endphp
|
||||
|
||||
<div class="container-item">
|
||||
<div class="container-header">
|
||||
<div class="container-info">
|
||||
<div class="container-avatar">
|
||||
{{ strtoupper(substr($container->container_name, 0, 2)) }}
|
||||
</div>
|
||||
<div class="container-details">
|
||||
<h3>{{ $container->container_name }}</h3>
|
||||
<div class="container-meta">
|
||||
<div class="meta-item">
|
||||
<i class="fas fa-hashtag"></i>
|
||||
<span>{{ $container->container_number }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<i class="fas fa-calendar"></i>
|
||||
<span>{{ $container->container_date?->format('M d, Y') ?: 'No date' }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<i class="fas fa-list"></i>
|
||||
<span>{{ $container->rows->count() }} entries</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
@can('containers.update_status')
|
||||
<form action="{{ route('containers.update-status', $container->id) }}"
|
||||
method="POST"
|
||||
class="update-form ajax-status-form"
|
||||
data-container-id="{{ $container->id }}">
|
||||
@csrf
|
||||
|
||||
@php $statusClass = 'status-' . $status; @endphp
|
||||
|
||||
<div class="status-dropdown">
|
||||
<div class="status-dropdown-toggle {{ $statusClass }}">
|
||||
<span class="status-dropdown-label">
|
||||
{{ $statusLabel }}
|
||||
</span>
|
||||
<i class="fas fa-chevron-down" style="font-size:11px;color:#4b5563;"></i>
|
||||
</div>
|
||||
<div class="status-dropdown-menu">
|
||||
@foreach($labels as $value => $label)
|
||||
@php $optClass = 'status-' . $value; @endphp
|
||||
<div class="status-option {{ $optClass }} {{ $status === $value ? 'active' : '' }}"
|
||||
data-status="{{ $value }}">
|
||||
<span class="dot"></span>
|
||||
<span>{{ $label }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endcan
|
||||
|
||||
@can('container.update')
|
||||
<a href="{{ route('containers.show', $container->id) }}" class="action-btn view-btn">
|
||||
<i class="fas fa-eye"></i> View
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
@can('container.delete')
|
||||
<form action="{{ route('containers.destroy', $container->id) }}" method="POST"
|
||||
class="delete-form"
|
||||
data-container-id="{{ $container->id }}">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="action-btn delete-btn">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</form>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="totals-section">
|
||||
<div class="total-card">
|
||||
<div class="total-value">{{ number_format($container->summary['total_ctn'], 1) }}</div>
|
||||
<div class="total-label">Total CTN</div>
|
||||
</div>
|
||||
<div class="total-card">
|
||||
<div class="total-value">{{ number_format($container->summary['total_qty'], 0) }}</div>
|
||||
<div class="total-label">Total QTY</div>
|
||||
</div>
|
||||
<div class="total-card">
|
||||
<div class="total-value">{{ number_format($container->summary['total_cbm'], 3) }}</div>
|
||||
<div class="total-label">Total CBM</div>
|
||||
</div>
|
||||
<div class="total-card">
|
||||
<div class="total-value">{{ number_format($container->summary['total_kg'], 1) }}</div>
|
||||
<div class="total-label">Total KG</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// STATUS DROPDOWN
|
||||
document.querySelectorAll('.status-dropdown').forEach(function (wrapper) {
|
||||
const toggle = wrapper.querySelector('.status-dropdown-toggle');
|
||||
const menu = wrapper.querySelector('.status-dropdown-menu');
|
||||
|
||||
toggle.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
document.querySelectorAll('.status-dropdown-menu.open').forEach(m => {
|
||||
if (m !== menu) m.classList.remove('open');
|
||||
});
|
||||
menu.classList.toggle('open');
|
||||
});
|
||||
|
||||
menu.querySelectorAll('.status-option').forEach(function (opt) {
|
||||
opt.addEventListener('click', function () {
|
||||
const status = this.dataset.status;
|
||||
const form = wrapper.closest('form');
|
||||
const labelEl = wrapper.querySelector('.status-dropdown-label');
|
||||
const toggleEl= wrapper.querySelector('.status-dropdown-toggle');
|
||||
|
||||
// UI: dropdown label + active item
|
||||
menu.querySelectorAll('.status-option').forEach(o => o.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
labelEl.textContent = this.querySelector('span:nth-child(2)').textContent;
|
||||
menu.classList.remove('open');
|
||||
|
||||
// toggle रंग class reset करून नवा status-* दे
|
||||
toggleEl.className = 'status-dropdown-toggle';
|
||||
toggleEl.classList.add('status-' + status);
|
||||
|
||||
const url = form.getAttribute('action');
|
||||
const token = form.querySelector('input[name="_token"]').value;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('_token', token);
|
||||
formData.append('status', status);
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(async res => {
|
||||
let data = null;
|
||||
try { data = await res.json(); } catch (e) {}
|
||||
if (!res.ok || !data || !data.success) {
|
||||
alert('Status update failed');
|
||||
return;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
alert('Network error while updating status');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', function () {
|
||||
document.querySelectorAll('.status-dropdown-menu.open')
|
||||
.forEach(m => m.classList.remove('open'));
|
||||
});
|
||||
|
||||
// DELETE VIA AJAX
|
||||
document.querySelectorAll('.delete-form').forEach(function (form) {
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!confirm('Are you sure you want to delete this container and all its entries?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = form.getAttribute('action');
|
||||
const token = form.querySelector('input[name="_token"]').value;
|
||||
const item = form.closest('.container-item');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('_token', token);
|
||||
formData.append('_method', 'DELETE');
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(async res => {
|
||||
let data = null;
|
||||
try { data = await res.json(); } catch (e) {}
|
||||
|
||||
if (!res.ok || !data || !data.success) {
|
||||
alert('Delete failed');
|
||||
return;
|
||||
}
|
||||
|
||||
if (item) {
|
||||
item.style.opacity = '0';
|
||||
item.style.transform = 'translateX(-10px)';
|
||||
setTimeout(() => item.remove(), 200);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
alert('Network error while deleting container');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
@endsection
|
||||
349
resources/views/admin/container_create.blade.php
Normal file
349
resources/views/admin/container_create.blade.php
Normal file
@@ -0,0 +1,349 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Add Container')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
.cm-add-wrapper { padding: 10px 0 20px 0; }
|
||||
.cm-add-header-card {
|
||||
border-radius: 14px;
|
||||
border: none;
|
||||
margin-bottom: 18px;
|
||||
background: linear-gradient(90deg,#4c6fff,#8e54e9);
|
||||
box-shadow: 0 6px 18px rgba(15,35,52,0.18);
|
||||
color: #ffffff;
|
||||
}
|
||||
.cm-add-header-card .card-body {
|
||||
padding: 14px 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
.cm-add-title { margin: 0; font-size: 20px; font-weight: 600; }
|
||||
.cm-add-sub { font-size: 12px; opacity: 0.9; }
|
||||
|
||||
.cm-add-main-card {
|
||||
border-radius: 14px;
|
||||
border: none;
|
||||
box-shadow: 0 6px 18px rgba(15,35,52,0.12);
|
||||
}
|
||||
.cm-add-main-card .card-header {
|
||||
background:#ffffff;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
padding: 10px 18px;
|
||||
}
|
||||
.cm-add-main-card .card-header h5 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cm-form-label { font-size: 13px; font-weight: 500; color:#495057; margin-bottom: 4px; }
|
||||
.cm-form-control {
|
||||
font-size: 13px;
|
||||
border-radius: 10px;
|
||||
border:1px solid #d0d7e2;
|
||||
padding: 8px 11px;
|
||||
}
|
||||
.cm-form-control:focus {
|
||||
border-color:#4c6fff;
|
||||
box-shadow:0 0 0 0.15rem rgba(76,111,255,.25);
|
||||
}
|
||||
|
||||
.cm-help-text { font-size: 11px; color:#868e96; margin-top: 2px; }
|
||||
.cm-btn-primary { border-radius: 20px; padding: 6px 22px; font-size: 13px; font-weight: 500; }
|
||||
.cm-btn-secondary { border-radius: 20px; padding: 6px 18px; font-size: 13px; }
|
||||
|
||||
.error-card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #f5c2c7;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.error-row-box {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.error-item {
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.error-item span {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container-fluid cm-add-wrapper">
|
||||
|
||||
<div class="card cm-add-header-card">
|
||||
<div class="card-body">
|
||||
<div>
|
||||
<h4 class="cm-add-title">Create New Container</h4>
|
||||
<div class="cm-add-sub">
|
||||
Add container details and upload Kent loading list Excel file.
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('containers.index') }}" class="btn btn-light btn-sm">
|
||||
Back to Containers
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card cm-add-main-card">
|
||||
<div class="card-header">
|
||||
<h5>Add Container</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
{{-- SUCCESS --}}
|
||||
@if (session('success'))
|
||||
<div class="alert alert-success">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- VALIDATION --}}
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<ul class="mb-0">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- COMBINED ERROR PANEL (summary text) --}}
|
||||
@if(session('formula_errors') || session('mark_errors'))
|
||||
|
||||
<div class="card error-card mb-3">
|
||||
<div class="card-header bg-danger text-white small">
|
||||
⚠ Excel Validation Issues Found
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="small mb-1">
|
||||
Some rows in your Excel file have formula or mark issues.
|
||||
See the table below, and detailed messages after the table.
|
||||
</div>
|
||||
<ul class="small mb-0 ps-3">
|
||||
<li>Red highlighted rows indicate formula mismatches in the uploaded Excel data.</li>
|
||||
<li>Yellow highlighted rows indicate marks from the Excel file that do not match any record in the system.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 1) Excel-style table at top --}}
|
||||
@php
|
||||
$formulaErrors = session('formula_errors') ?? [];
|
||||
$markErrors = session('mark_errors') ?? [];
|
||||
|
||||
$allRowsData = [];
|
||||
|
||||
foreach ($formulaErrors as $fe) {
|
||||
if (!empty($fe['data'] ?? null)) {
|
||||
$allRowsData[] = $fe['data'];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($markErrors as $me) {
|
||||
if (!empty($me['data'] ?? null)) {
|
||||
$allRowsData[] = $me['data'];
|
||||
}
|
||||
}
|
||||
|
||||
$headings = [];
|
||||
if (!empty($allRowsData)) {
|
||||
$headings = array_keys($allRowsData[0]);
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if(!empty($headings))
|
||||
<div class="card mb-3">
|
||||
<div class="card-header small">
|
||||
Error rows in Excel view
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
<div class="table-responsive" style="max-height:260px; overflow:auto;">
|
||||
<table class="table table-sm table-bordered mb-0" style="font-size:11.5px;">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Excel Row</th>
|
||||
<th>Mark No</th>
|
||||
@foreach($headings as $head)
|
||||
<th>{{ $head }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{-- Formula error rows (red – formula mismatch, critical) --}}
|
||||
@foreach($formulaErrors as $fe)
|
||||
@php $rowData = $fe['data'] ?? []; @endphp
|
||||
@if(!empty($rowData))
|
||||
<tr class="table-danger">
|
||||
<td>{{ $fe['excel_row'] }}</td>
|
||||
<td>{{ $fe['mark_no'] }}</td>
|
||||
@foreach($headings as $head)
|
||||
<td>{{ $rowData[$head] ?? '' }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
{{-- Mark error rows (yellow – mark not found, warning) --}}
|
||||
@foreach($markErrors as $me)
|
||||
@php $rowData = $me['data'] ?? []; @endphp
|
||||
@if(!empty($rowData))
|
||||
<tr class="table-warning">
|
||||
<td>{{ $me['excel_row'] }}</td>
|
||||
<td>{{ $me['mark_no'] }}</td>
|
||||
@foreach($headings as $head)
|
||||
<td>{{ $rowData[$head] ?? '' }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endif
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 2) Detailed per-row error boxes BELOW the table --}}
|
||||
<div class="card error-card mb-3">
|
||||
<div class="card-header bg-light small">
|
||||
Detailed error messages
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
{{-- Formula Errors (detailed) --}}
|
||||
@if(session('formula_errors'))
|
||||
@foreach(session('formula_errors') as $fe)
|
||||
<div class="error-row-box">
|
||||
<div class="error-title">
|
||||
Row {{ $fe['excel_row'] }}
|
||||
@if($fe['mark_no']) | Mark: {{ $fe['mark_no'] }} @endif
|
||||
@if($fe['description']) | {{ $fe['description'] }} @endif
|
||||
</div>
|
||||
|
||||
@foreach($fe['errors'] as $field => $detail)
|
||||
<div class="error-item text-danger">
|
||||
❌ <span>{{ $field }}</span> →
|
||||
Expected: {{ number_format($detail['expected'],4) }}
|
||||
| Got: {{ number_format($detail['actual'],4) }}
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
{{-- Mark Errors (detailed) --}}
|
||||
@if(session('mark_errors'))
|
||||
@foreach(session('mark_errors') as $me)
|
||||
<div class="error-row-box bg-warning bg-opacity-10 border-warning">
|
||||
<div class="error-title">
|
||||
Row {{ $me['excel_row'] }}
|
||||
</div>
|
||||
<div class="error-item text-warning">
|
||||
❌ Mark <strong>{{ $me['mark_no'] }}</strong> not found in database
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endif
|
||||
|
||||
{{-- FORM --}}
|
||||
@if (!session('formula_errors') && !session('mark_errors'))
|
||||
<form action="{{ route('containers.store') }}" method="POST" enctype="multipart/form-data" class="mt-3">
|
||||
@csrf
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="cm-form-label">Container Name</label>
|
||||
<input type="text" name="container_name"
|
||||
class="form-control cm-form-control"
|
||||
value="{{ old('container_name') }}"
|
||||
placeholder="Enter container name">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="cm-form-label">Container Number</label>
|
||||
<input type="text" name="container_number"
|
||||
class="form-control cm-form-control"
|
||||
value="{{ old('container_number') }}"
|
||||
placeholder="Enter container number">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="cm-form-label">Container Date</label>
|
||||
<input type="date"
|
||||
name="container_date" {{-- name fix --}}
|
||||
id="containerdate" {{-- JS साठी जुना id ठेवला --}}
|
||||
class="form-control cm-form-control"
|
||||
value="{{ old('container_date') }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="cm-form-label">Loading List Excel</label>
|
||||
<input type="file" name="excel_file"
|
||||
class="form-control cm-form-control"
|
||||
accept=".xls,.xlsx">
|
||||
<div class="cm-help-text">
|
||||
Upload Kent loading list Excel file (.xls / .xlsx).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary cm-btn-primary">
|
||||
Save Container
|
||||
</button>
|
||||
<a href="{{ route('containers.index') }}" class="btn btn-outline-secondary cm-btn-secondary">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const dateInput = document.getElementById('containerdate');
|
||||
if (!dateInput) return;
|
||||
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
const todayStr = `${year}-${month}-${day}`;
|
||||
|
||||
// Todays date
|
||||
dateInput.min = todayStr;
|
||||
|
||||
// old date remove
|
||||
if (dateInput.value && dateInput.value < todayStr) {
|
||||
dateInput.value = todayStr;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
251
resources/views/admin/container_pdf.blade.php
Normal file
251
resources/views/admin/container_pdf.blade.php
Normal file
@@ -0,0 +1,251 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Container {{ $container->container_number }} Summary</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: DejaVu Sans, sans-serif;
|
||||
font-size: 10px;
|
||||
margin: 10px;
|
||||
background: #e5e7ff;
|
||||
}
|
||||
|
||||
/* LOGO HEADER */
|
||||
.logo-header-wrap {
|
||||
width: 100%;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.logo-header-inner {
|
||||
display: table;
|
||||
}
|
||||
.logo-header-logo-cell {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.logo-header-text-cell {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
padding-left: 8px;
|
||||
}
|
||||
.logo-header-logo {
|
||||
height: 32px; /* banner type, कमी उंची */
|
||||
object-fit: contain;
|
||||
}
|
||||
.logo-header-title-top {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #7b1111; /* dark maroon */
|
||||
line-height: 1.1;
|
||||
}
|
||||
.logo-header-title-bottom {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #7b1111;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* COMMON CARD GRID – 4 equal columns, 2 rows */
|
||||
.card-grid {
|
||||
display: table;
|
||||
width: 100%;
|
||||
border-spacing: 8px 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.card-row {
|
||||
display: table-row;
|
||||
}
|
||||
.card {
|
||||
display: table-cell;
|
||||
width: 25%;
|
||||
padding: 7px 10px;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 12px rgba(15,35,52,0.18);
|
||||
color: #0f172a;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* INFO CARDS (FIRST ROW) */
|
||||
.info-id { background: #e0f2ff; border-left: 4px solid #2563eb; }
|
||||
.info-no { background: #dcfce7; border-left: 4px solid #22c55e; }
|
||||
.info-date { background: #fee2e2; border-left: 4px solid #ef4444; }
|
||||
.info-name { background: #fef9c3; border-left: 4px solid #f59e0b; }
|
||||
|
||||
/* TOTAL CARDS (SECOND ROW) */
|
||||
.total-ctn { background: #dbeafe; border-left: 4px solid #1d4ed8; }
|
||||
.total-qty { background: #bbf7d0; border-left: 4px solid #16a34a; }
|
||||
.total-cbm { background: #fef3c7; border-left: 4px solid #d97706; }
|
||||
.total-kg { background: #fecaca; border-left: 4px solid #dc2626; }
|
||||
|
||||
.label-text {
|
||||
font-size: 9px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
color: #4b5563;
|
||||
}
|
||||
.value-text-small {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
margin-top: 2px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.value-text-big {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* TABLE – solid yellow header */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 6px;
|
||||
table-layout: fixed;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 4px 3px;
|
||||
text-align: center;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
th {
|
||||
background: #fbd85d;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
color: #0c0909;
|
||||
}
|
||||
td {
|
||||
font-size: 9px;
|
||||
color: #111827;
|
||||
}
|
||||
tr:nth-child(even) td {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
thead { display: table-header-group; }
|
||||
tr { page-break-inside: avoid; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@php
|
||||
$totalCtn = 0;
|
||||
$totalQty = 0;
|
||||
$totalCbm = 0.0;
|
||||
$totalKg = 0.0;
|
||||
|
||||
foreach ($container->rows as $row) {
|
||||
if (!is_array($row->data)) continue;
|
||||
foreach ($row->data as $h => $v) {
|
||||
$norm = strtoupper(str_replace([' ', '/', '-', '.'],'', $h));
|
||||
$val = is_numeric(str_replace([','], '', $v)) ? floatval(str_replace([','], '', $v)) : 0;
|
||||
|
||||
if (str_contains($norm, 'TOTALCTN') || $norm === 'CTN' || str_contains($norm,'TOTALCNTR') || str_contains($norm,'TOTALCARTON')) {
|
||||
$totalCtn += $val;
|
||||
}
|
||||
if (str_contains($norm,'TOTALQTY') || str_contains($norm,'ITLQTY') || str_contains($norm,'TTLQTY')) {
|
||||
$totalQty += $val;
|
||||
}
|
||||
if (str_contains($norm,'TOTALCBM') || str_contains($norm,'TTLCBM') || str_contains($norm,'ITLCBM')) {
|
||||
$totalCbm += $val;
|
||||
}
|
||||
if (str_contains($norm,'TOTALKG') || str_contains($norm,'TTKG')) {
|
||||
$totalKg += $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$allHeadings = [];
|
||||
foreach ($container->rows as $row) {
|
||||
if (is_array($row->data)) {
|
||||
$allHeadings = array_unique(array_merge($allHeadings, array_keys($row->data)));
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
{{-- LOGO + TEXT – full left, text खाली (reference image) --}}
|
||||
<div class="logo-header-wrap">
|
||||
<div class="logo-header-inner">
|
||||
<div class="logo-header-logo-cell">
|
||||
<img src="{{ public_path('images/kentlogo1.png') }}"
|
||||
class="logo-header-logo"
|
||||
alt="Kent Logo">
|
||||
</div>
|
||||
<div class="logo-header-text-cell">
|
||||
<div class="logo-header-title-top">KENT</div>
|
||||
<div class="logo-header-title-bottom">International Pvt. Ltd.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- TWO ROW GRID – FIRST: INFO / SECOND: TOTALS --}}
|
||||
<div class="card-grid">
|
||||
<div class="card-row">
|
||||
<div class="card info-id">
|
||||
<div class="label-text">Container ID</div>
|
||||
<div class="value-text-small">{{ $container->id }}</div>
|
||||
</div>
|
||||
<div class="card info-no">
|
||||
<div class="label-text">Container Number</div>
|
||||
<div class="value-text-small">{{ $container->container_number }}</div>
|
||||
</div>
|
||||
<div class="card info-date">
|
||||
<div class="label-text">Container Date</div>
|
||||
<div class="value-text-small">
|
||||
{{ $container->container_date ? $container->container_date->format('d-m-Y') : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card info-name">
|
||||
<div class="label-text">Container Name</div>
|
||||
<div class="value-text-small">
|
||||
{{ $container->container_name ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-row">
|
||||
<div class="card total-ctn">
|
||||
<div class="label-text">Total CTN</div>
|
||||
<div class="value-text-big">{{ number_format($totalCtn, 0) }}</div>
|
||||
</div>
|
||||
<div class="card total-qty">
|
||||
<div class="label-text">Total QTY</div>
|
||||
<div class="value-text-big">{{ number_format($totalQty, 0) }}</div>
|
||||
</div>
|
||||
<div class="card total-cbm">
|
||||
<div class="label-text">Total CBM</div>
|
||||
<div class="value-text-big">{{ number_format($totalCbm, 3) }}</div>
|
||||
</div>
|
||||
<div class="card total-kg">
|
||||
<div class="label-text">Total KG</div>
|
||||
<div class="value-text-big">{{ number_format($totalKg, 2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- FULL TABLE --}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:18px;">#</th>
|
||||
@foreach($allHeadings as $heading)
|
||||
<th>{{ $heading }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($container->rows as $index => $row)
|
||||
<tr>
|
||||
<td>{{ $index + 1 }}</td>
|
||||
@foreach($allHeadings as $heading)
|
||||
<td>{{ $row->data[$heading] ?? '' }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
966
resources/views/admin/container_show.blade.php
Normal file
966
resources/views/admin/container_show.blade.php
Normal file
@@ -0,0 +1,966 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Container Details')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap');
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.cm-header-card {
|
||||
background: linear-gradient(100deg, #4c6fff 0%, #8e54e9 100%);
|
||||
border-radius: 14px;
|
||||
border: none;
|
||||
box-shadow: 0 6px 24px rgba(76,111,255,0.22);
|
||||
margin-bottom: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
.cm-header-card .card-body {
|
||||
padding: 14px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.cm-header-title {
|
||||
margin: 0;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
.cm-header-sub {
|
||||
font-size: 12px;
|
||||
opacity: 0.88;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.cm-download-pdf {
|
||||
background: linear-gradient(100deg, #4c6fff 0%, #8e54e9 100%) !important;
|
||||
color: #fff !important;
|
||||
border: none !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 12.5px !important;
|
||||
padding: 8px 16px !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 14px rgba(76,111,255,0.4) !important;
|
||||
transition: all 0.2s ease !important;
|
||||
text-decoration: none !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
gap: 6px !important;
|
||||
}
|
||||
.cm-download-pdf:hover {
|
||||
transform: translateY(-1px) !important;
|
||||
box-shadow: 0 6px 20px rgba(76,111,255,0.5) !important;
|
||||
color: #fff !important;
|
||||
opacity: 0.95 !important;
|
||||
}
|
||||
|
||||
.cm-download-excel {
|
||||
background: linear-gradient(100deg, #10b981 0%, #059669 100%) !important;
|
||||
color: #fff !important;
|
||||
border: none !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 12.5px !important;
|
||||
padding: 8px 16px !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 14px rgba(16,185,129,0.4) !important;
|
||||
transition: all 0.2s ease !important;
|
||||
text-decoration: none !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
gap: 6px !important;
|
||||
}
|
||||
.cm-download-excel:hover {
|
||||
transform: translateY(-1px) !important;
|
||||
box-shadow: 0 6px 20px rgba(16,185,129,0.5) !important;
|
||||
color: #fff !important;
|
||||
opacity: 0.95 !important;
|
||||
}
|
||||
|
||||
.cm-back-btn {
|
||||
background: linear-gradient(100deg, #6b7280 0%, #4b5563 100%) !important;
|
||||
color: #fff !important;
|
||||
border: none !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 12.5px !important;
|
||||
padding: 8px 16px !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 14px rgba(107,114,128,0.4) !important;
|
||||
transition: all 0.2s ease !important;
|
||||
text-decoration: none !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
gap: 6px !important;
|
||||
}
|
||||
.cm-back-btn:hover {
|
||||
transform: translateY(-1px) !important;
|
||||
box-shadow: 0 6px 20px rgba(107,114,128,0.5) !important;
|
||||
color: #fff !important;
|
||||
opacity: 0.95 !important;
|
||||
}
|
||||
|
||||
.cm-main-card {
|
||||
border-radius: 14px;
|
||||
border: none;
|
||||
box-shadow: 0 4px 20px rgba(15,35,52,0.10);
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
}
|
||||
.cm-main-card .card-header {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.cm-main-card .card-header h5 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1a2340;
|
||||
}
|
||||
|
||||
.cm-info-cards-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
padding: 14px 18px 8px 18px;
|
||||
}
|
||||
.cm-info-card {
|
||||
border-radius: 16px;
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
box-shadow: 0 4px 16px rgba(15,35,52,0.12);
|
||||
border: 1px solid rgba(148,163,184,0.25);
|
||||
background: #fff;
|
||||
min-height: 70px;
|
||||
}
|
||||
.cm-info-card-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #0f172a;
|
||||
font-size: 16px;
|
||||
box-shadow: 0 3px 10px rgba(15,23,42,0.10);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cm-info-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
.cm-info-card-label {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cm-info-card-value {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cm-card-container {
|
||||
background: linear-gradient(135deg, #e0f2ff, #eef4ff);
|
||||
}
|
||||
.cm-card-container .cm-info-card-icon {
|
||||
background: linear-gradient(135deg, #2563eb, #4f46e5);
|
||||
color: #e5edff;
|
||||
}
|
||||
.cm-card-date {
|
||||
background: linear-gradient(135deg, #ecfdf3, #e0fbea);
|
||||
}
|
||||
.cm-card-date .cm-info-card-icon {
|
||||
background: linear-gradient(135deg, #16a34a, #22c55e);
|
||||
color: #ecfdf3;
|
||||
}
|
||||
.cm-card-excel {
|
||||
background: linear-gradient(135deg, #fff7ed, #fffbeb);
|
||||
}
|
||||
.cm-card-excel .cm-info-card-icon {
|
||||
background: linear-gradient(135deg, #f97316, #fb923c);
|
||||
color: #fff7ed;
|
||||
}
|
||||
|
||||
.cm-total-cards-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
padding: 4px 18px 14px 18px;
|
||||
}
|
||||
.cm-total-card {
|
||||
border-radius: 18px;
|
||||
padding: 12px 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
box-shadow: 0 8px 30px rgba(15,23,42,0.08);
|
||||
border: 1px solid rgba(148,163,184,0.25);
|
||||
}
|
||||
.cm-total-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 17px;
|
||||
flex-shrink: 0;
|
||||
color: #0f172a;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 14px rgba(15,23,42,0.15);
|
||||
}
|
||||
.cm-total-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
.cm-total-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #4b5563;
|
||||
}
|
||||
.cm-total-value {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: #111827;
|
||||
}
|
||||
.cm-total-card-ctn {
|
||||
background: linear-gradient(135deg, #e0f2ff, #eef2ff);
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
.cm-total-card-qty {
|
||||
background: linear-gradient(135deg, #dcfce7, #ecfdf5);
|
||||
border-left: 4px solid #22c55e;
|
||||
}
|
||||
.cm-total-card-cbm {
|
||||
background: linear-gradient(135deg, #fef9c3, #fffbeb);
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
.cm-total-card-kg {
|
||||
background: linear-gradient(135deg, #fee2e2, #fef2f2);
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
|
||||
.cm-filter-bar {
|
||||
padding: 4px 20px 0 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.cm-row-count {
|
||||
font-size: 12px;
|
||||
color: #8a93a6;
|
||||
font-weight: 500;
|
||||
}
|
||||
.cm-filter-input {
|
||||
max-width: 240px;
|
||||
font-size: 12px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #dde2ee;
|
||||
padding: 6px 14px;
|
||||
outline: none;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
.cm-filter-input:focus {
|
||||
border-color: #4c6fff;
|
||||
box-shadow: 0 0 0 3px rgba(76,111,255,0.1);
|
||||
}
|
||||
|
||||
.cm-table-scroll-outer {
|
||||
margin: 10px 14px 30px 14px;
|
||||
border-radius: 14px;
|
||||
border: 1.5px solid #c9a359;
|
||||
box-shadow: 0 4px 14px rgba(76,111,255,0.18);
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.cm-table-wrapper {
|
||||
position: relative;
|
||||
min-width: 1200px;
|
||||
}
|
||||
|
||||
.cm-table {
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
}
|
||||
.cm-table thead tr th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
background: linear-gradient(100deg, #fde4b3 0%, #fbd48a 100%);
|
||||
color: #0c0909;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
padding: 11px 14px;
|
||||
border-bottom: 2px solid rgba(255,255,255,0.15);
|
||||
border-right: 1px solid rgba(255,255,255,0.18);
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
letter-spacing: 0.2px;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.35);
|
||||
}
|
||||
.cm-table thead tr th:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
.cm-table thead tr th:last-child {
|
||||
border-top-right-radius: 10px;
|
||||
border-right: none;
|
||||
}
|
||||
.cm-table thead tr th:first-child,
|
||||
.cm-table tbody tr td:first-child {
|
||||
width: 46px;
|
||||
min-width: 46px;
|
||||
max-width: 46px;
|
||||
text-align: center;
|
||||
}
|
||||
.cm-table tbody tr td {
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid #f0f3fb;
|
||||
border-right: 1px solid #f0f3fb;
|
||||
color: #2d3a55;
|
||||
font-size: 12.5px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
background: #fff;
|
||||
transition: background 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cm-table tbody tr td:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
.cm-table tbody tr:nth-child(even) td {
|
||||
background: #f8f9ff;
|
||||
}
|
||||
.cm-table tbody tr:hover td {
|
||||
background: #edf3ff !important;
|
||||
}
|
||||
.cm-row-num {
|
||||
color: #8a93a6;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cm-cell-input {
|
||||
width: 100%;
|
||||
min-width: 90px;
|
||||
background: transparent;
|
||||
border: 1.5px solid transparent;
|
||||
border-radius: 5px;
|
||||
font-size: 12.5px;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
color: #2d3a55;
|
||||
padding: 3px 6px;
|
||||
text-align: center;
|
||||
transition: border 0.15s, background 0.15s, box-shadow 0.15s;
|
||||
outline: none;
|
||||
}
|
||||
.cm-cell-input:hover {
|
||||
border-color: #c9d4f5;
|
||||
background: #f5f8ff;
|
||||
}
|
||||
.cm-cell-input:focus {
|
||||
border-color: #4c6fff;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 3px rgba(76,111,255,0.12);
|
||||
}
|
||||
.cm-cell-readonly {
|
||||
background: #f3f4ff;
|
||||
cursor: not-allowed;
|
||||
border-color: #e2e3ff;
|
||||
}
|
||||
|
||||
.cm-save-btn-floating {
|
||||
position: fixed;
|
||||
right: 26px;
|
||||
bottom: 22px;
|
||||
z-index: 50;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border-radius: 22px;
|
||||
padding: 8px 20px;
|
||||
background: linear-gradient(90deg, #4c6fff, #8e54e9);
|
||||
border: none;
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 16px rgba(76,111,255,0.35);
|
||||
transition: opacity 0.2s, transform 0.15s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.cm-save-btn-floating:hover {
|
||||
opacity: 0.94;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.cm-save-btn-floating.cm-disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cm-toast {
|
||||
position: fixed;
|
||||
right: 26px;
|
||||
bottom: 70px;
|
||||
background: #0f172a;
|
||||
color: #f9fafb;
|
||||
font-size: 12.5px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 8px 24px rgba(15,23,42,0.25);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s, transform 0.2s;
|
||||
transform: translateY(8px);
|
||||
z-index: 60;
|
||||
}
|
||||
.cm-toast.cm-show {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.cm-empty {
|
||||
padding: 30px 20px;
|
||||
color: #8a93a6;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cm-table-scroll-outer::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
.cm-table-scroll-outer::-webkit-scrollbar-track {
|
||||
background: #f0f3fb;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.cm-table-scroll-outer::-webkit-scrollbar-thumb {
|
||||
background: #c5cce8;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.cm-table-scroll-outer::-webkit-scrollbar-thumb:hover {
|
||||
background: #8a99d0;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.cm-total-cards-row {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.cm-header-card .card-body {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.cm-info-cards-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.cm-table-scroll-outer {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.cm-table-wrapper {
|
||||
min-width: 900px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 575px) {
|
||||
.cm-total-cards-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container-fluid cm-wrapper">
|
||||
<div class="card cm-header-card">
|
||||
<div class="card-body">
|
||||
<div>
|
||||
<h4 class="cm-header-title">Container {{ $container->container_number }}</h4>
|
||||
<div class="cm-header-sub">
|
||||
Edit loading list directly – like Excel. TT columns auto‑calculate from CTN, QTY, CBM, KG, PRICE.
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ route('containers.index') }}" class="cm-back-btn">
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
Back to list
|
||||
</a>
|
||||
|
||||
<a href="{{ route('containers.download.pdf', $container->id) }}" class="cm-download-pdf">
|
||||
<i class="bi bi-file-earmark-pdf"></i>
|
||||
Download PDF
|
||||
</a>
|
||||
|
||||
<a href="{{ route('containers.download.excel', $container->id) }}" class="cm-download-excel">
|
||||
<i class="bi bi-file-earmark-excel"></i>
|
||||
Download Excel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card cm-main-card">
|
||||
<div class="card-header">
|
||||
<h5>Container Information</h5>
|
||||
</div>
|
||||
|
||||
<div class="cm-info-cards-row">
|
||||
<div class="cm-info-card cm-card-container">
|
||||
<div class="cm-info-card-icon">
|
||||
<i class="bi bi-box-seam"></i>
|
||||
</div>
|
||||
<div class="cm-info-card-body">
|
||||
<div class="cm-info-card-label">Container</div>
|
||||
<div class="cm-info-card-value">
|
||||
{{ $container->container_name ?? $container->container_number }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cm-info-card cm-card-date">
|
||||
<div class="cm-info-card-icon">
|
||||
<i class="bi bi-calendar-event"></i>
|
||||
</div>
|
||||
<div class="cm-info-card-body">
|
||||
<div class="cm-info-card-label">Date</div>
|
||||
<div class="cm-info-card-value">
|
||||
{{ $container->container_date ? $container->container_date->format('d-m-Y') : '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cm-info-card cm-card-excel">
|
||||
<div class="cm-info-card-icon">
|
||||
<i class="bi bi-file-earmark-excel"></i>
|
||||
</div>
|
||||
<div class="cm-info-card-body">
|
||||
<div class="cm-info-card-label">Excel File</div>
|
||||
<div class="cm-info-card-value">
|
||||
@if($container->excel_file)
|
||||
<a href="{{ url($container->excel_file) }}" target="_blank" style="color:#0f172a;text-decoration:none;">
|
||||
Download / View
|
||||
</a>
|
||||
@else
|
||||
<span style="color:#b0b8cc;">Not uploaded</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$totalCtn = 0;
|
||||
$totalQty = 0;
|
||||
$totalCbm = 0.0;
|
||||
$totalKg = 0.0;
|
||||
|
||||
if(!$container->rows->isEmpty()){
|
||||
foreach ($container->rows as $row) {
|
||||
if (!is_array($row->data)) continue;
|
||||
foreach ($row->data as $h => $v) {
|
||||
$norm = strtoupper(str_replace([' ', '/', '-', '.'],'', $h));
|
||||
$val = is_numeric(str_replace([','], '', $v)) ? floatval(str_replace([','], '', $v)) : 0;
|
||||
|
||||
if (str_contains($norm, 'TOTALCTN') || $norm === 'CTN' || str_contains($norm,'TOTALCNTR') || str_contains($norm,'TOTALCARTON')) {
|
||||
$totalCtn += $val;
|
||||
}
|
||||
|
||||
if (
|
||||
str_contains($norm,'TOTALQTY') ||
|
||||
str_contains($norm,'ITLQTY') ||
|
||||
str_contains($norm,'TTLQTY')
|
||||
) {
|
||||
$totalQty += $val;
|
||||
}
|
||||
|
||||
if (
|
||||
str_contains($norm,'TOTALCBM') ||
|
||||
str_contains($norm,'TTLCBM') ||
|
||||
str_contains($norm,'ITLCBM')
|
||||
) {
|
||||
$totalCbm += $val;
|
||||
}
|
||||
|
||||
if (
|
||||
str_contains($norm,'TOTALKG') ||
|
||||
str_contains($norm,'TTKG')
|
||||
) {
|
||||
$totalKg += $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if(!$container->rows->isEmpty())
|
||||
<div class="cm-total-cards-row">
|
||||
<div class="cm-total-card cm-total-card-ctn">
|
||||
<div class="cm-total-icon">
|
||||
<i class="bi bi-box"></i>
|
||||
</div>
|
||||
<div class="cm-total-text">
|
||||
<div class="cm-total-label">Total CTN</div>
|
||||
<div class="cm-total-value">
|
||||
{{ number_format($totalCtn, 0) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cm-total-card cm-total-card-qty">
|
||||
<div class="cm-total-icon">
|
||||
<i class="bi bi-check-circle"></i>
|
||||
</div>
|
||||
<div class="cm-total-text">
|
||||
<div class="cm-total-label">Total QTY</div>
|
||||
<div class="cm-total-value">
|
||||
{{ number_format($totalQty, 0) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cm-total-card cm-total-card-cbm">
|
||||
<div class="cm-total-icon">
|
||||
<i class="bi bi-border-width"></i>
|
||||
</div>
|
||||
<div class="cm-total-text">
|
||||
<div class="cm-total-label">Total CBM</div>
|
||||
<div class="cm-total-value">
|
||||
{{ number_format($totalCbm, 3) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cm-total-card cm-total-card-kg">
|
||||
<div class="cm-total-icon">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
</div>
|
||||
<div class="cm-total-text">
|
||||
<div class="cm-total-label">Total KG</div>
|
||||
<div class="cm-total-value">
|
||||
{{ number_format($totalKg, 2) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($container->rows->isEmpty())
|
||||
<div class="cm-empty">No entries found for this container.</div>
|
||||
@else
|
||||
@php
|
||||
$allHeadings = [];
|
||||
foreach ($container->rows as $row) {
|
||||
if (is_array($row->data)) {
|
||||
$allHeadings = array_unique(array_merge($allHeadings, array_keys($row->data)));
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="cm-filter-bar">
|
||||
<span class="cm-row-count">
|
||||
Total rows: {{ $container->rows->count() }} Edit cells then click "Save Changes".
|
||||
</span>
|
||||
<input type="text" id="cmRowSearch" class="cm-filter-input"
|
||||
placeholder="Quick search..." onkeyup="cmFilterRows()">
|
||||
</div>
|
||||
|
||||
<form id="cm-edit-rows-form" action="{{ route('containers.rows.update', $container->id) }}" method="POST">
|
||||
@csrf
|
||||
<div class="cm-table-scroll-outer">
|
||||
<div class="cm-table-wrapper">
|
||||
<table class="cm-table" id="cmExcelTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
@foreach($allHeadings as $heading)
|
||||
<th>{{ $heading }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($container->rows as $index => $row)
|
||||
<tr>
|
||||
<td class="cm-row-num">{{ $index + 1 }}</td>
|
||||
@foreach($allHeadings as $heading)
|
||||
@php
|
||||
$value = $row->data[$heading] ?? '';
|
||||
|
||||
$norm = strtoupper(str_replace([' ', '/', '-', '.'],'', $heading));
|
||||
|
||||
$isCtn = str_contains($norm, 'CTN');
|
||||
|
||||
$isTotalQty = (
|
||||
str_contains($norm, 'TOTALQTY') ||
|
||||
str_contains($norm, 'ITLQTY') ||
|
||||
str_contains($norm, 'TTLQTY')
|
||||
);
|
||||
$isQty = !$isTotalQty && (
|
||||
str_contains($norm, 'QTY') ||
|
||||
str_contains($norm, 'PCS') ||
|
||||
str_contains($norm, 'PIECES')
|
||||
);
|
||||
|
||||
$isTotalCbm = (
|
||||
str_contains($norm, 'TOTALCBM') ||
|
||||
str_contains($norm, 'TTLCBM') ||
|
||||
str_contains($norm, 'ITLCBM')
|
||||
);
|
||||
$isCbm = !$isTotalCbm && str_contains($norm, 'CBM');
|
||||
|
||||
$isTotalKg = (
|
||||
str_contains($norm, 'TOTALKG') ||
|
||||
str_contains($norm, 'TTKG')
|
||||
);
|
||||
$isKg = !$isTotalKg && (str_contains($norm, 'KG') || str_contains($norm, 'WEIGHT'));
|
||||
|
||||
$isPrice = (str_contains($norm, 'PRICE') || str_contains($norm, 'RATE'));
|
||||
|
||||
$isAmount = (
|
||||
str_contains($norm, 'AMOUNT') ||
|
||||
str_contains($norm, 'TTLAMOUNT') ||
|
||||
str_contains($norm, 'TOTALAMOUNT')
|
||||
);
|
||||
|
||||
$isTotalColumn = $isTotalQty || $isTotalCbm || $isTotalKg || $isAmount;
|
||||
$isLockedByInvoice = in_array($row->row_index, $lockedRowIndexes ?? []);
|
||||
$isReadOnly = $isTotalColumn || $container->status !== 'pending' || $isLockedByInvoice;
|
||||
@endphp
|
||||
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="cm-cell-input {{ $isReadOnly ? 'cm-cell-readonly' : '' }}"
|
||||
name="rows[{{ $row->id }}][{{ $heading }}]"
|
||||
value="{{ $value }}"
|
||||
data-row-id="{{ $row->id }}"
|
||||
data-col-key="{{ $heading }}"
|
||||
data-ctn="{{ $isCtn ? '1' : '0' }}"
|
||||
data-qty="{{ $isQty ? '1' : '0' }}"
|
||||
data-ttlqty="{{ $isTotalQty ? '1' : '0' }}"
|
||||
data-cbm="{{ $isCbm ? '1' : '0' }}"
|
||||
data-ttlcbm="{{ $isTotalCbm ? '1' : '0' }}"
|
||||
data-kg="{{ $isKg ? '1' : '0' }}"
|
||||
data-ttlkg="{{ $isTotalKg ? '1' : '0' }}"
|
||||
data-price="{{ $isPrice ? '1' : '0' }}"
|
||||
data-amount="{{ $isAmount ? '1' : '0' }}"
|
||||
{{ $isReadOnly ? 'readonly' : '' }}
|
||||
>
|
||||
</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!$container->rows->isEmpty())
|
||||
<button
|
||||
id="cmSaveBtnFloating"
|
||||
class="cm-save-btn-floating {{ $container->status !== 'pending' ? 'cm-disabled' : '' }}"
|
||||
{{ $container->status !== 'pending' ? 'disabled' : '' }}
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<div id="cmToast" class="cm-toast"></div>
|
||||
|
||||
<script>
|
||||
function cmFilterRows() {
|
||||
const input = document.getElementById('cmRowSearch');
|
||||
if (!input) return;
|
||||
const filter = input.value.toLowerCase();
|
||||
const table = document.getElementById('cmExcelTable');
|
||||
if (!table) return;
|
||||
const rows = table.getElementsByTagName('tr');
|
||||
for (let i = 1; i < rows.length; i++) {
|
||||
const cells = rows[i].getElementsByTagName('td');
|
||||
let match = false;
|
||||
for (let j = 0; j < cells.length; j++) {
|
||||
const txt = cells[j].textContent || cells[j].innerText;
|
||||
if (txt.toLowerCase().indexOf(filter) > -1) {
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
rows[i].style.display = match ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const form = document.getElementById('cm-edit-rows-form');
|
||||
const btn = document.getElementById('cmSaveBtnFloating');
|
||||
const toast = document.getElementById('cmToast');
|
||||
const table = document.getElementById('cmExcelTable');
|
||||
|
||||
function showToast(message, isError = false) {
|
||||
if (!toast) return;
|
||||
toast.textContent = message;
|
||||
toast.style.background = isError ? '#b91c1c' : '#0f172a';
|
||||
toast.classList.add('cm-show');
|
||||
setTimeout(() => toast.classList.remove('cm-show'), 2500);
|
||||
}
|
||||
|
||||
// फक्त number काढण्यासाठी helper, पण cell मध्ये format नाही करणार
|
||||
function parseNumber(str) {
|
||||
if (!str) return 0;
|
||||
const cleaned = String(str).replace(/,/g, '').trim();
|
||||
const val = parseFloat(cleaned);
|
||||
return isNaN(val) ? 0 : val;
|
||||
}
|
||||
|
||||
function recalcRow(row) {
|
||||
const inputs = row.querySelectorAll('.cm-cell-input');
|
||||
|
||||
let ctn = 0, qty = 0, ttlQty = 0;
|
||||
let cbm = 0, ttlCbm = 0;
|
||||
let kg = 0, ttlKg = 0;
|
||||
let price = 0, amount = 0;
|
||||
|
||||
let ctnInput = null,
|
||||
qtyInput = null,
|
||||
ttlQtyInput = null,
|
||||
cbmInput = null,
|
||||
ttlCbmInput = null,
|
||||
kgInput = null,
|
||||
ttlKgInput = null,
|
||||
priceInput = null,
|
||||
amountInput = null;
|
||||
|
||||
inputs.forEach(inp => {
|
||||
const val = parseNumber(inp.value);
|
||||
|
||||
if (inp.dataset.ctn === '1') {
|
||||
ctn = val;
|
||||
ctnInput = inp;
|
||||
} else if (inp.dataset.qty === '1') {
|
||||
qty = val;
|
||||
qtyInput = inp;
|
||||
} else if (inp.dataset.ttlqty === '1') {
|
||||
ttlQty = val;
|
||||
ttlQtyInput = inp;
|
||||
} else if (inp.dataset.cbm === '1') {
|
||||
cbm = val;
|
||||
cbmInput = inp;
|
||||
} else if (inp.dataset.ttlcbm === '1') {
|
||||
ttlCbm = val;
|
||||
ttlCbmInput = inp;
|
||||
} else if (inp.dataset.kg === '1') {
|
||||
kg = val;
|
||||
kgInput = inp;
|
||||
} else if (inp.dataset.ttlkg === '1') {
|
||||
ttlKg = val;
|
||||
ttlKgInput = inp;
|
||||
} else if (inp.dataset.price === '1') {
|
||||
price = val;
|
||||
priceInput = inp;
|
||||
} else if (inp.dataset.amount === '1') {
|
||||
amount = val;
|
||||
amountInput = inp;
|
||||
}
|
||||
});
|
||||
|
||||
// इथे आपण फक्त VALUE बदलतो, कोणतंही toFixed नाही वापरत
|
||||
if (ttlQtyInput && ctnInput && qtyInput) {
|
||||
const newTtlQty = ctn * qty;
|
||||
ttlQtyInput.value = newTtlQty === 0 ? '' : String(newTtlQty);
|
||||
ttlQty = newTtlQty;
|
||||
}
|
||||
|
||||
if (ttlCbmInput && cbmInput && ctnInput) {
|
||||
const newTtlCbm = cbm * ctn;
|
||||
ttlCbmInput.value = newTtlCbm === 0 ? '' : String(newTtlCbm);
|
||||
ttlCbm = newTtlCbm;
|
||||
}
|
||||
|
||||
if (ttlKgInput && kgInput && ctnInput) {
|
||||
const newTtlKg = kg * ctn;
|
||||
ttlKgInput.value = newTtlKg === 0 ? '' : String(newTtlKg);
|
||||
ttlKg = newTtlKg;
|
||||
}
|
||||
|
||||
if (amountInput && priceInput && ttlQtyInput) {
|
||||
const newAmount = price * ttlQty;
|
||||
amountInput.value = newAmount === 0 ? '' : String(newAmount);
|
||||
amount = newAmount;
|
||||
}
|
||||
}
|
||||
|
||||
if (table) {
|
||||
table.addEventListener('input', function (e) {
|
||||
const target = e.target;
|
||||
if (!target.classList.contains('cm-cell-input')) return;
|
||||
|
||||
if (target.classList.contains('cm-cell-readonly') || target.hasAttribute('readonly')) {
|
||||
target.blur();
|
||||
return;
|
||||
}
|
||||
|
||||
const row = target.closest('tr');
|
||||
if (row) {
|
||||
recalcRow(row);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (form && btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
if (btn.classList.contains('cm-disabled') || btn.hasAttribute('disabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
btn.classList.add('cm-disabled');
|
||||
const formData = new FormData(form);
|
||||
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-TOKEN': document.querySelector('input[name="_token"]').value
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(async res => {
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text || 'Failed to save');
|
||||
}
|
||||
return res.json().catch(() => ({}));
|
||||
})
|
||||
.then(() => {
|
||||
showToast('Changes saved successfully.');
|
||||
})
|
||||
.catch(() => {
|
||||
showToast('Error while saving changes.', true);
|
||||
})
|
||||
.finally(() => {
|
||||
btn.classList.remove('cm-disabled');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
overflow-x: hidden; /* Prevent horizontal scroll on body */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
@@ -22,7 +22,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* New Stats Container */
|
||||
.stats-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
@@ -43,9 +42,7 @@
|
||||
min-height: 70px;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.stat-card:hover { transform: translateY(-2px); }
|
||||
|
||||
.stat-card.warning {
|
||||
border-left-color: #f59e0b;
|
||||
@@ -57,16 +54,6 @@
|
||||
background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
|
||||
}
|
||||
|
||||
.stat-card.danger {
|
||||
border-left-color: #ef4444;
|
||||
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
|
||||
}
|
||||
|
||||
.stat-card.info {
|
||||
border-left-color: #3b82f6;
|
||||
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
|
||||
}
|
||||
|
||||
.stat-card.secondary {
|
||||
border-left-color: #8b5cf6;
|
||||
background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);
|
||||
@@ -91,46 +78,19 @@
|
||||
.stat-card.warning .stat-icon {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
.stat-card.warning .stat-icon i {
|
||||
color: #f59e0b;
|
||||
}
|
||||
.stat-card.warning .stat-icon i { color: #f59e0b; }
|
||||
|
||||
.stat-card.success .stat-icon {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
.stat-card.success .stat-icon i {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.stat-card.danger .stat-icon {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.stat-card.danger .stat-icon i {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.stat-card.info .stat-icon {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.stat-card.info .stat-icon i {
|
||||
color: #3b82f6;
|
||||
}
|
||||
.stat-card.success .stat-icon i { color: #10b981; }
|
||||
|
||||
.stat-card.secondary .stat-icon {
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
}
|
||||
.stat-card.secondary .stat-icon i { color: #8b5cf6; }
|
||||
|
||||
.stat-card.secondary .stat-icon i {
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
.stat-content { flex: 1; }
|
||||
|
||||
.stat-value {
|
||||
font-size: 22px;
|
||||
@@ -147,14 +107,13 @@
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Updated Search Container - Wider with icon on left */
|
||||
.search-container {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 10px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
width: 350px; /* Increased width */
|
||||
width: 350px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -210,7 +169,6 @@
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Updated Table Styles - Fixed horizontal scroll */
|
||||
.table-glass {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 10px;
|
||||
@@ -219,7 +177,6 @@
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Single gradient for entire header - Blue to Purple */
|
||||
.table thead {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||
}
|
||||
@@ -232,20 +189,9 @@
|
||||
border: none;
|
||||
font-family: 'Inter', sans-serif;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #667eea 0%);;
|
||||
|
||||
background: linear-gradient(135deg, #667eea 0%);
|
||||
}
|
||||
|
||||
/* Remove individual curved borders */
|
||||
.table-header:first-child {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
.table-header:last-child {
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
/* Apply rounded corners to the entire header container */
|
||||
.table-container thead tr:first-child th:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
@@ -254,7 +200,6 @@
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
/* Updated Table Column Alignment */
|
||||
.table > :not(caption) > * > * {
|
||||
padding: 14px 12px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
@@ -263,44 +208,35 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Center align specific columns */
|
||||
.table > :not(caption) > * > *:nth-child(2), /* Customer ID */
|
||||
.table > :not(caption) > * > *:nth-child(3), /* Orders */
|
||||
.table > :not(caption) > * > *:nth-child(4), /* Total */
|
||||
.table > :not(caption) > * > *:nth-child(5) { /* Create Date */
|
||||
.table > :not(caption) > * > *:nth-child(2),
|
||||
.table > :not(caption) > * > *:nth-child(3),
|
||||
.table > :not(caption) > * > *:nth-child(4),
|
||||
.table > :not(caption) > * > *:nth-child(5) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Customer Info column should remain left-aligned */
|
||||
.table > :not(caption) > * > *:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
.table > :not(caption) > * > *:first-child { text-align: left; }
|
||||
|
||||
/* Status and Actions columns should remain as is */
|
||||
.table > :not(caption) > * > *:nth-child(6), /* Status */
|
||||
.table > :not(caption) > * > *:nth-child(7) { /* Actions */
|
||||
.table > :not(caption) > * > *:nth-child(6),
|
||||
.table > :not(caption) > * > *:nth-child(7),
|
||||
.table > :not(caption) > * > *:nth-child(8),
|
||||
.table > :not(caption) > * > *:nth-child(9) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Updated header alignment to match */
|
||||
|
||||
.table-header:nth-child(2),
|
||||
.table-header:nth-child(3),
|
||||
.table-header:nth-child(4),
|
||||
.table-header:nth-child(5) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Customer Info header stays left */
|
||||
.table-header:first-child {
|
||||
text-align: Center;
|
||||
}
|
||||
|
||||
/* Status and Actions headers stay centered */
|
||||
.table-header:nth-child(5),
|
||||
.table-header:nth-child(6),
|
||||
.table-header:nth-child(7) {
|
||||
.table-header:nth-child(7),
|
||||
.table-header:nth-child(8),
|
||||
.table-header:nth-child(9) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table-header:first-child { text-align: Center; }
|
||||
|
||||
.customer-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -374,7 +310,7 @@
|
||||
|
||||
.customer-info-column {
|
||||
min-width: 220px;
|
||||
max-width: 220px; /* Added max-width to prevent overflow */
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.table tbody tr {
|
||||
@@ -391,31 +327,25 @@
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Remove customer-stats since we're adding columns */
|
||||
|
||||
/* Enhanced table styling - Fixed horizontal scroll */
|
||||
.table-container {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
width: 100%; /* Ensure container takes full width */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Fix table responsiveness */
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Ensure table doesn't exceed container */
|
||||
.table {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 0;
|
||||
table-layout: auto; /* Changed to auto for better column distribution */
|
||||
table-layout: auto;
|
||||
}
|
||||
|
||||
/* Fix for search and filter section */
|
||||
.search-filter-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -439,11 +369,10 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* New columns styling */
|
||||
.orders-column, .total-column, .customer-id-column, .create-date-column {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
min-width: 80px; /* Added minimum widths for consistency */
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.orders-count {
|
||||
@@ -458,7 +387,6 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---------- Pagination Styles ---------- */
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -485,14 +413,10 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
.pagination-img-btn {
|
||||
background: #fff;
|
||||
border: 1px solid #e3eaf6;
|
||||
color: #1a2951;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
@@ -500,20 +424,19 @@
|
||||
justify-content: center;
|
||||
min-width: 40px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
.pagination-img-btn:hover:not(:disabled) {
|
||||
background: #1a2951;
|
||||
color: white;
|
||||
border-color: #1a2951;
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
.pagination-img-btn:disabled {
|
||||
background: #f8fafc;
|
||||
color: #cbd5e0;
|
||||
border-color: #e2e8f0;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.pagination-page-btn {
|
||||
@@ -551,61 +474,11 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination-ellipsis {
|
||||
color: #9ba5bb;
|
||||
font-size: 13px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
/* Image-based pagination buttons */
|
||||
.pagination-img-btn {
|
||||
background: #fff;
|
||||
border: 1px solid #e3eaf6;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 40px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pagination-img-btn:hover:not(:disabled) {
|
||||
background: #1a2951;
|
||||
border-color: #1a2951;
|
||||
}
|
||||
|
||||
.pagination-img-btn:disabled {
|
||||
background: #f8fafc;
|
||||
border-color: #e2e8f0;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.pagination-img-btn img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
filter: brightness(0) saturate(100%) invert(26%) sepia(89%) saturate(748%) hue-rotate(201deg) brightness(93%) contrast(89%);
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.pagination-img-btn:hover:not(:disabled) img {
|
||||
filter: brightness(0) saturate(100%) invert(100%) sepia(100%) saturate(0%) hue-rotate(288deg) brightness(106%) contrast(101%);
|
||||
}
|
||||
|
||||
.pagination-img-btn:disabled img {
|
||||
filter: brightness(0) saturate(100%) invert(84%) sepia(8%) saturate(165%) hue-rotate(179deg) brightness(89%) contrast(86%);
|
||||
}
|
||||
|
||||
/* Mobile responsive fixes */
|
||||
@media (max-width: 1200px) {
|
||||
.table > :not(caption) > * > * {
|
||||
padding: 12px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.customer-info-column {
|
||||
min-width: 180px;
|
||||
max-width: 180px;
|
||||
@@ -613,65 +486,42 @@
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.search-container {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.stats-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.search-container { width: 280px; }
|
||||
.stats-container { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.stats-container { grid-template-columns: repeat(2, 1fr); }
|
||||
.search-filter-container {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-section { justify-content: center; }
|
||||
.search-container { width: 100%; }
|
||||
.filter-section {
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pagination-controls { justify-content: center; }
|
||||
.table > :not(caption) > * > * {
|
||||
padding: 10px 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.customer-info-column {
|
||||
min-width: 150px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.customer-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
@@ -680,19 +530,12 @@
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.stats-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stats-container { grid-template-columns: 1fr; }
|
||||
.table-responsive { font-size: 12px; }
|
||||
.customer-info-column {
|
||||
min-width: 120px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.premium-badge,
|
||||
.regular-badge,
|
||||
.status-badge {
|
||||
@@ -703,14 +546,11 @@
|
||||
</style>
|
||||
|
||||
<div class="container-fluid">
|
||||
<!-- Header - Removed gradient -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 style="color: #2c3e50; font-weight: 700; font-family: 'Inter', sans-serif;">Customer List</h4>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards with NEW DESIGN -->
|
||||
<div class="stats-container">
|
||||
<!-- Total Customers -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<i class="bi bi-people-fill"></i>
|
||||
@@ -721,7 +561,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New This Month -->
|
||||
<div class="stat-card warning">
|
||||
<div class="stat-icon">
|
||||
<i class="bi bi-person-plus"></i>
|
||||
@@ -739,7 +578,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Customers -->
|
||||
<div class="stat-card success">
|
||||
<div class="stat-icon">
|
||||
<i class="bi bi-activity"></i>
|
||||
@@ -755,7 +593,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Premium Customers -->
|
||||
<div class="stat-card secondary">
|
||||
<div class="stat-icon">
|
||||
<i class="bi bi-award-fill"></i>
|
||||
@@ -772,10 +609,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter Section -->
|
||||
<div class="glass-card p-3 mb-3">
|
||||
<div class="search-filter-container">
|
||||
<!-- Search Section - Wider with icon on left -->
|
||||
<div class="search-section">
|
||||
<form method="GET" action="{{ route('admin.customers.index') }}" class="d-flex align-items-center">
|
||||
<div class="search-container">
|
||||
@@ -792,7 +627,6 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Filter Section -->
|
||||
<div class="filter-section">
|
||||
<a href="{{ route('admin.customers.index', ['status'=>'active', 'search'=>$search ?? '']) }}"
|
||||
class="filter-btn {{ ($status ?? '') == 'active' ? 'active' : '' }}">
|
||||
@@ -818,7 +652,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customer List Table -->
|
||||
<div class="table-container">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
@@ -827,86 +660,132 @@
|
||||
<th class="table-header">Customer Info</th>
|
||||
<th class="table-header">Customer ID</th>
|
||||
<th class="table-header">Orders</th>
|
||||
<th class="table-header">Total</th>
|
||||
<th class="table-header">Order Total</th>
|
||||
<th class="table-header">GST Amount</th>
|
||||
<th class="table-header">Total Paid</th>
|
||||
<th class="table-header">Remaining</th>
|
||||
<th class="table-header">Create Date</th>
|
||||
<th class="table-header">Status</th>
|
||||
<th class="table-header" width="100">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody id="customersTableBody">
|
||||
@forelse($customers as $c)
|
||||
<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 }}
|
||||
@php
|
||||
// 1) Orders = total invoice count
|
||||
$ordersCount = $c->invoices->count();
|
||||
|
||||
// 2) Order Total = सर्व invoices च्या charge groups चा base total (without GST)
|
||||
$orderTotal = $c->invoices->sum(function($invoice) {
|
||||
return $invoice->chargeGroups->sum('total_charge');
|
||||
});
|
||||
|
||||
// 3) GST Amount = सर्व invoices च्या gst_amount चा sum
|
||||
$gstTotal = $c->invoices->sum('gst_amount');
|
||||
|
||||
// 3) Total Payable = customer ने किती paid केले (installments sum)
|
||||
$totalPaid = $c->invoices->flatMap->installments->sum('amount');
|
||||
|
||||
// 4) Remaining = grand_total_with_charges - paid
|
||||
$grandTotal = $c->invoices->sum(function($invoice) {
|
||||
$base = $invoice->chargeGroups->sum('total_charge');
|
||||
$gst = (float)($invoice->gst_amount ?? 0);
|
||||
return $base + $gst;
|
||||
});
|
||||
$remainingAmount = max($grandTotal - $totalPaid, 0);
|
||||
@endphp
|
||||
<tr>
|
||||
<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($orderTotal, 2) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Create Date -->
|
||||
<td class="create-date-column">
|
||||
<span class="text-muted">{{ $c->created_at ? $c->created_at->format('d-m-Y') : '-' }}</span>
|
||||
</td>
|
||||
<td class="total-column">
|
||||
<span class="total-amount">
|
||||
₹{{ number_format($gstTotal, 2) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Status -->
|
||||
<td>
|
||||
@if($c->status === 'active')
|
||||
<span class="status-badge active-status">Active</span>
|
||||
@else
|
||||
<span class="status-badge inactive-status">Inactive</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="total-column">
|
||||
<span class="total-amount">
|
||||
₹{{ number_format($totalPaid, 2) }}
|
||||
</span>
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4">
|
||||
<td colspan="10" class="text-center py-4">
|
||||
<i class="bi bi-people display-4 text-muted d-block mb-2"></i>
|
||||
<span class="text-muted">No customers found.</span>
|
||||
</td>
|
||||
@@ -917,14 +796,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
<div class="pagination-container">
|
||||
<div class="pagination-info" id="pageInfo">
|
||||
Showing {{ $customers->firstItem() ?? 0 }} to {{ $customers->lastItem() ?? 0 }} of {{ $customers->total() }} entries
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<button class="pagination-img-btn" id="prevPageBtn" title="Previous page" {{ $customers->onFirstPage() ? 'disabled' : '' }}>
|
||||
<!-- Left arrow SVG -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 12L6 8L10 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
@@ -938,7 +815,6 @@
|
||||
@endfor
|
||||
</div>
|
||||
<button class="pagination-img-btn" id="nextPageBtn" title="Next page" {{ $customers->hasMorePages() ? '' : 'disabled' }}>
|
||||
<!-- Right arrow SVG -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 4L10 8L6 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
@@ -949,7 +825,6 @@
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add hover effects to table rows
|
||||
const tableRows = document.querySelectorAll('.table tbody tr');
|
||||
tableRows.forEach(row => {
|
||||
row.addEventListener('mouseenter', function() {
|
||||
@@ -961,7 +836,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Pagination button handlers
|
||||
document.getElementById('prevPageBtn').addEventListener('click', function() {
|
||||
@if(!$customers->onFirstPage())
|
||||
window.location.href = '{{ $customers->previousPageUrl() }}';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user