Compare commits

..

11 Commits

Author SHA256 Message Date
Utkarsh Khedkar
19d7f423b3 pdf Updated, Invoice Updated, Report Updated 2026-03-17 19:14:47 +05:30
Utkarsh Khedkar
0257b68f16 Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2026-03-13 23:06:51 +05:30
Utkarsh Khedkar
785f2564be changes 2026-03-13 23:06:19 +05:30
Abhishek Mali
9a6ca49ad7 user order controller update for show view data 2026-03-13 20:19:34 +05:30
Abhishek Mali
bf2689e62d container and invoice api done small api like view invoice traking in order section is remaning 2026-03-13 17:25:58 +05:30
Utkarsh Khedkar
c25b468c77 Status Updated paying 2026-03-12 18:20:09 +05:30
Utkarsh Khedkar
5d8a746876 Status Updated 2026-03-12 18:11:43 +05:30
Utkarsh Khedkar
bb2a361a97 Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2026-03-12 12:34:49 +05:30
Utkarsh Khedkar
6b5876e08f Fetch Data For Customer Details 2026-03-12 12:34:27 +05:30
Abhishek Mali
43b1a64911 ajax update 2026-03-12 11:48:42 +05:30
Utkarsh Khedkar
ff4c006ca4 Gst Updates 2026-03-11 20:02:43 +05:30
58 changed files with 4330 additions and 2967 deletions

View File

@@ -1,6 +1,6 @@
APP_NAME=Laravel APP_NAME=Laravel
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=base64:/ulhRgiCOFjZV6xUDkXLfiR9X8iFRZ4QIiX3UJbdwY4=
APP_DEBUG=true APP_DEBUG=true
APP_URL=http://localhost APP_URL=http://localhost

View File

@@ -5,6 +5,10 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; 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 class AdminAuthController extends Controller
{ {
@@ -31,16 +35,15 @@ class AdminAuthController extends Controller
} }
$credentials = [ $credentials = [
$field => $loginInput, $field => $loginInput,
'password' => $request->password, 'password' => $request->password,
]; ];
// attempt login
if (Auth::guard('admin')->attempt($credentials)) { if (Auth::guard('admin')->attempt($credentials)) {
$request->session()->regenerate(); $request->session()->regenerate();
$user = Auth::guard('admin')->user(); $user = Auth::guard('admin')->user();
return redirect()->route('admin.dashboard')
return redirect()->route('admin.dashboard')->with('success', 'Welcome back, ' . $user->name . '!'); ->with('success', 'Welcome back, ' . $user->name . '!');
} }
return back()->withErrors(['login' => 'Invalid login credentials.']); return back()->withErrors(['login' => 'Invalid login credentials.']);
@@ -51,6 +54,25 @@ class AdminAuthController extends Controller
Auth::guard('admin')->logout(); Auth::guard('admin')->logout();
$request->session()->invalidate(); $request->session()->invalidate();
$request->session()->regenerateToken(); $request->session()->regenerateToken();
return redirect()->route('admin.login')->with('success', 'Logged out successfully.'); return redirect()->route('admin.login')
->with('success', 'Logged out successfully.');
}
public function profile()
{
$user = Auth::guard('admin')->user();
// ── Real Stats ──
$stats = [
'total_containers' => Container::count(),
'total_invoices' => Invoice::count(),
'paid_invoices' => Invoice::where('status', 'paid')->count(),
'pending_invoices' => Invoice::where('status', 'pending')->count(),
'total_customers' => User::count(),
'total_marklist' => MarkList::count(),
'active_marklist' => MarkList::where('status', 'active')->count(),
];
return view('admin.profile', compact('user', 'stats'));
} }
} }

View File

@@ -22,7 +22,8 @@ class AdminCustomerController extends Controller
$query = User::with([ $query = User::with([
'marks', 'marks',
'orders', 'orders',
'invoices.installments' // 🔥 IMPORTANT 'invoices.installments',
'invoices.chargeGroups', // 🔥 for order total calculation
])->orderBy('id', 'desc'); ])->orderBy('id', 'desc');
if (!empty($search)) { if (!empty($search)) {

View File

@@ -2,33 +2,31 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller; // ⬅️ हे नक्की असू दे
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\InvoiceItem; use App\Models\InvoiceItem;
use App\Models\InvoiceInstallment;
use App\Models\InvoiceChargeGroup; use App\Models\InvoiceChargeGroup;
use App\Models\InvoiceChargeGroupItem; use App\Models\InvoiceChargeGroupItem;
use App\Models\User; use App\Models\InvoiceInstallment;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Mpdf\Mpdf; use Mpdf\Mpdf;
class AdminInvoiceController extends Controller class AdminInvoiceController extends Controller
{ {
// ------------------------------------------------------------- // -------------------------------------------------------------
// INVOICE LIST PAGE // INDEX (LIST ALL INVOICES WITH FILTERS)
// ------------------------------------------------------------- // -------------------------------------------------------------
public function index(Request $request) public function index(Request $request)
{ {
// Container relation सह invoices load करतो $query = Invoice::query();
$query = Invoice::with(['items', 'customer', 'container']);
if ($request->filled('search')) { if ($request->filled('search')) {
$search = $request->search; $search = $request->search;
$query->where(function ($q) use ($search) { $query->where(function ($q) use ($search) {
$q->where('invoice_number', 'like', "%{$search}%") $q->where('invoice_number', 'like', "%{$search}%")
->orWhere('customer_name', 'like', "%{$search}%"); ->orWhere('customer_name', 'like', "%{$search}%");
}); });
} }
@@ -50,7 +48,7 @@ class AdminInvoiceController extends Controller
} }
// ------------------------------------------------------------- // -------------------------------------------------------------
// POPUP VIEW + CUSTOMER DATA SYNC // POPUP VIEW
// ------------------------------------------------------------- // -------------------------------------------------------------
public function popup($id) public function popup($id)
{ {
@@ -59,16 +57,8 @@ class AdminInvoiceController extends Controller
'chargeGroups.items', 'chargeGroups.items',
])->findOrFail($id); ])->findOrFail($id);
// demo update असेल तर ठेव/काढ
$invoice->update([
'customer_email' => 'test@demo.com',
'customer_address' => 'TEST ADDRESS',
'pincode' => '999999',
]);
$shipment = null; $shipment = null;
// आधीच group मध्ये असलेले item ids
$groupedItemIds = $invoice->chargeGroups $groupedItemIds = $invoice->chargeGroups
->flatMap(fn($group) => $group->items->pluck('invoice_item_id')) ->flatMap(fn($group) => $group->items->pluck('invoice_item_id'))
->unique() ->unique()
@@ -88,8 +78,29 @@ class AdminInvoiceController extends Controller
'customer', 'customer',
'container', 'container',
'chargeGroups.items', 'chargeGroups.items',
'installments',
])->findOrFail($id); ])->findOrFail($id);
// ✅ 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; $shipment = null;
$groupedItemIds = $invoice->chargeGroups $groupedItemIds = $invoice->chargeGroups
@@ -104,107 +115,60 @@ class AdminInvoiceController extends Controller
} }
// ------------------------------------------------------------- // -------------------------------------------------------------
// UPDATE INVOICE (HEADER LEVEL) // UPDATE INVOICE (HEADER ONLY)
// ------------------------------------------------------------- // -------------------------------------------------------------
public function update(Request $request, $id) public function update(Request $request, $id)
{ {
Log::info('🟡 Invoice Update Request Received', [ Log::info('🟡 Invoice Update Request Received', [
'invoice_id' => $id, 'invoice_id' => $id,
'request' => $request->all(), 'request' => $request->all(),
]); ]);
$invoice = Invoice::findOrFail($id); $invoice = Invoice::findOrFail($id);
$data = $request->validate([ $data = $request->validate([
'invoice_date' => 'required|date', 'invoice_date' => 'required|date',
'due_date' => 'required|date|after_or_equal:invoice_date', 'due_date' => 'required|date|after_or_equal:invoice_date',
'final_amount' => 'required|numeric|min:0', 'status' => 'required|in:pending,paying,paid,overdue',
'tax_type' => 'required|in:gst,igst', 'notes' => 'nullable|string',
'tax_percent' => 'required|numeric|min:0|max:28',
'status' => 'required|in:pending,paid,overdue',
'notes' => 'nullable|string',
]); ]);
Log::info('✅ Validated Invoice Update Data', $data); Log::info('✅ Validated Invoice Header Update Data', $data);
$finalAmount = (float) $data['final_amount'];
$taxPercent = (float) $data['tax_percent'];
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;
}
$gstAmount = ($finalAmount * $taxPercent) / 100;
$data['gst_amount'] = $gstAmount;
$data['final_amount_with_gst'] = $finalAmount + $gstAmount;
$data['gst_percent'] = $taxPercent;
Log::info('📌 Final Calculated Invoice Values', [
'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'],
]);
$invoice->update($data); $invoice->update($data);
Log::info('✅ Invoice Updated Successfully', [
'invoice_id' => $invoice->id,
]);
$invoice->refresh(); $invoice->refresh();
Log::info('🔍 Invoice AFTER UPDATE (DB values)', [
'invoice_id' => $invoice->id, Log::info('🔍 Invoice AFTER HEADER UPDATE', [
'final_amount' => $invoice->final_amount, 'invoice_id' => $invoice->id,
'gst_percent' => $invoice->gst_percent, 'charge_groups_total' => $invoice->charge_groups_total,
'gst_amount' => $invoice->gst_amount, 'gst_amount' => $invoice->gst_amount,
'final_amount_with_gst' => $invoice->final_amount_with_gst, 'grand_total_with_charges'=> $invoice->grand_total_with_charges,
'tax_type' => $invoice->tax_type,
'cgst_percent' => $invoice->cgst_percent,
'sgst_percent' => $invoice->sgst_percent,
'igst_percent' => $invoice->igst_percent,
]); ]);
$this->generateInvoicePDF($invoice); $this->generateInvoicePDF($invoice);
return redirect() return redirect()
->route('admin.invoices.index') ->route('admin.invoices.edit', $invoice->id)
->with('success', 'Invoice updated & PDF generated successfully.'); ->with('success', 'Invoice updated & PDF generated successfully.');
} }
// ------------------------------------------------------------- // -------------------------------------------------------------
// UPDATE INVOICE ITEMS // UPDATE INVOICE ITEMS (फक्त items save)
// ------------------------------------------------------------- // -------------------------------------------------------------
public function updateItems(Request $request, Invoice $invoice) public function updateItems(Request $request, Invoice $invoice)
{ {
Log::info('🟡 Invoice Items Update Request', [ Log::info('🟡 Invoice Items Update Request', [
'invoice_id' => $invoice->id, 'invoice_id' => $invoice->id,
'payload' => $request->all(), 'payload' => $request->all(),
]); ]);
$data = $request->validate([ $data = $request->validate([
'items' => ['required', 'array'], 'items' => ['required', 'array'],
'items.*.price' => ['required', 'numeric', 'min:0'], 'items.*.price' => ['required', 'numeric', 'min:0'],
'items.*.ttl_amount' => ['required', 'numeric', 'min:0'], 'items.*.ttl_amount' => ['required', 'numeric', 'min:0'],
]); ]);
$itemsInput = $data['items']; foreach ($data['items'] as $itemId => $itemData) {
foreach ($itemsInput as $itemId => $itemData) {
$item = InvoiceItem::where('id', $itemId) $item = InvoiceItem::where('id', $itemId)
->where('invoice_id', $invoice->id) ->where('invoice_id', $invoice->id)
->first(); ->first();
@@ -212,72 +176,34 @@ class AdminInvoiceController extends Controller
if (!$item) { if (!$item) {
Log::warning('Invoice item not found or mismatched invoice', [ Log::warning('Invoice item not found or mismatched invoice', [
'invoice_id' => $invoice->id, 'invoice_id' => $invoice->id,
'item_id' => $itemId, 'item_id' => $itemId,
]); ]);
continue; continue;
} }
$item->price = $itemData['price']; $item->price = $itemData['price'];
$item->ttl_amount = $itemData['ttl_amount']; $item->ttl_amount = $itemData['ttl_amount'];
$item->save(); $item->save();
} }
$newBaseAmount = InvoiceItem::where('invoice_id', $invoice->id) Log::info('✅ Invoice items updated (no totals recalculation)', [
->sum('ttl_amount'); 'invoice_id' => $invoice->id,
$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();
// ⭐ Total Charges (groups समावेत) पुन्हा कॅलक्युलेट
$chargeGroupsTotal = $invoice->chargeGroups()->sum('total_charge');
$invoice->charge_groups_total = $chargeGroupsTotal;
$invoice->grand_total_with_charges = $invoice->final_amount_with_gst + $chargeGroupsTotal;
$invoice->save();
Log::info('✅ Invoice items updated & totals recalculated', [
'invoice_id' => $invoice->id,
'final_amount' => $invoice->final_amount,
'gst_amount' => $invoice->gst_amount,
'final_amount_with_gst' => $invoice->final_amount_with_gst,
'tax_type' => $invoice->tax_type,
'cgst_percent' => $invoice->cgst_percent,
'sgst_percent' => $invoice->sgst_percent,
'igst_percent' => $invoice->igst_percent,
'charge_groups_total' => $invoice->charge_groups_total,
'grand_total_with_charges' => $invoice->grand_total_with_charges,
]); ]);
return back()->with('success', 'Invoice items updated successfully.'); return back()->with('success', 'Invoice items updated successfully.');
} }
// ------------------------------------------------------------- // -------------------------------------------------------------
// PDF GENERATION // PDF GENERATION (EXISTING - केवळ chargeGroups load ला confirm कर)
// ------------------------------------------------------------- // -------------------------------------------------------------
public function generateInvoicePDF($invoice) public function generateInvoicePDF($invoice)
{ {
$invoice->load(['items', 'customer', 'container']); // ✅ यामध्ये chargeGroups आणि installments load कर
$invoice->load(['items', 'customer', 'container', 'chargeGroups.items', 'installments']);
$shipment = null; $shipment = null;
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf'; $fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
$folder = public_path('invoices/'); $folder = public_path('invoices/');
if (!file_exists($folder)) { if (!file_exists($folder)) {
mkdir($folder, 0777, true); mkdir($folder, 0777, true);
@@ -290,13 +216,13 @@ class AdminInvoiceController extends Controller
} }
$mpdf = new Mpdf([ $mpdf = new Mpdf([
'mode' => 'utf-8', 'mode' => 'utf-8',
'format' => 'A4', 'format' => 'A4',
'default_font' => 'sans-serif', 'default_font' => 'sans-serif',
]); ]);
$html = view('admin.pdf.invoice', [ $html = view('admin.pdf.invoice', [
'invoice' => $invoice, 'invoice' => $invoice,
'shipment' => $shipment, 'shipment' => $shipment,
])->render(); ])->render();
@@ -313,51 +239,69 @@ class AdminInvoiceController extends Controller
{ {
$request->validate([ $request->validate([
'installment_date' => 'required|date', 'installment_date' => 'required|date',
'payment_method' => 'required|string', 'payment_method' => 'required|string',
'reference_no' => 'nullable|string', 'reference_no' => 'nullable|string',
'amount' => 'required|numeric|min:1', 'amount' => 'required|numeric|min:1',
]); ]);
$invoice = Invoice::findOrFail($invoice_id); $invoice = Invoice::findOrFail($invoice_id);
$paidTotal = $invoice->installments()->sum('amount');
// 👇 Total Charges (grand_total_with_charges) वरून remaining $grandTotal = $invoice->grand_total_with_charges ?? 0;
$grandTotal = $invoice->grand_total_with_charges;
$remaining = $grandTotal - $paidTotal; $paidTotal = $invoice->installments()->sum('amount');
$remaining = $grandTotal - $paidTotal;
if ($request->amount > $remaining) { if ($request->amount > $remaining) {
return response()->json([ return response()->json([
'status' => 'error', 'status' => 'error',
'message' => 'Installment amount exceeds remaining balance.', 'message' => 'Installment amount exceeds remaining balance.',
], 422); ], 422);
} }
$installment = InvoiceInstallment::create([ $installment = InvoiceInstallment::create([
'invoice_id' => $invoice_id, 'invoice_id' => $invoice_id,
'installment_date' => $request->installment_date, 'installment_date' => $request->installment_date,
'payment_method' => $request->payment_method, 'payment_method' => $request->payment_method,
'reference_no' => $request->reference_no, 'reference_no' => $request->reference_no,
'amount' => $request->amount, 'amount' => $request->amount,
]); ]);
$newPaid = $paidTotal + $request->amount; $newPaid = $paidTotal + $request->amount;
$remaining = max(0, $grandTotal - $newPaid);
if ($newPaid >= $invoice->final_amount_with_gst) { $isOverdue = now()->startOfDay()->gt(\Carbon\Carbon::parse($invoice->due_date)->startOfDay());
$invoice->update(['status' => 'paid']);
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';
} }
return response()->json([ $invoice->update([
'status' => 'success', 'payment_method' => $request->payment_method,
'message' => 'Installment added successfully.', 'reference_no' => $request->reference_no,
'installment' => $installment, 'status' => $newStatus,
'totalPaid' => $newPaid,
'gstAmount' => $invoice->gst_amount,
'finalAmountWithGst' => $grandTotal, // इथे grand total पाठव
'baseAmount' => $invoice->final_amount,
'remaining' => max(0, $grandTotal - $newPaid),
'isCompleted' => $newPaid >= $grandTotal,
]); ]);
return response()->json([
'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,
]);
} }
// ------------------------------------------------------------- // -------------------------------------------------------------
@@ -366,90 +310,195 @@ class AdminInvoiceController extends Controller
public function deleteInstallment($id) public function deleteInstallment($id)
{ {
$installment = InvoiceInstallment::findOrFail($id); $installment = InvoiceInstallment::findOrFail($id);
$invoice = $installment->invoice; $invoice = $installment->invoice;
$installment->delete(); $installment->delete();
$invoice->refresh();
$paidTotal = $invoice->installments()->sum('amount'); $grandTotal = $invoice->grand_total_with_charges ?? 0;
$grandTotal = $invoice->grand_total_with_charges;
$remaining = $grandTotal - $paidTotal; $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([ return response()->json([
'status' => 'success', 'status' => 'success',
'message' => 'Installment deleted.', 'message' => 'Installment deleted.',
'totalPaid' => $paidTotal, 'chargeGroupsTotal' => $invoice->charge_groups_total ?? 0,
'gstAmount' => $invoice->gst_amount, 'gstAmount' => $invoice->gst_amount ?? 0,
'finalAmountWithGst' => $grandTotal, // इथेही 'grandTotal' => $grandTotal,
'baseAmount' => $invoice->final_amount, 'totalPaid' => $paidTotal,
'remaining' => $remaining, 'remaining' => $remaining,
'isZero' => $paidTotal == 0, 'newStatus' => $newStatus,
'isZero' => $paidTotal == 0,
]); ]);
} }
// ------------------------------------------------------------- // -------------------------------------------------------------
// CHARGE GROUP SAVE (no AJAX branch) // CHARGE GROUP SAVE
// ------------------------------------------------------------- // -------------------------------------------------------------
public function storeChargeGroup(Request $request, $invoiceId) public function storeChargeGroup(Request $request, $invoiceId)
{ {
$invoice = Invoice::with('items')->findOrFail($invoiceId); Log::info('🟡 storeChargeGroup HIT', [
'invoice_id' => $invoiceId,
$data = $request->validate([ 'payload' => $request->all(),
'group_name' => 'required|string|max:255',
'basis_type' => 'required|in:ttl_qty,amount,ttl_cbm,ttl_kg',
'basis_value' => 'required|numeric',
'rate' => 'required|numeric|min:0.0001',
'auto_total' => 'required|numeric|min:0.01',
'item_ids' => 'required|array',
'item_ids.*' => 'integer|exists:invoice_items,id',
]); ]);
$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) $exists = InvoiceChargeGroup::where('invoice_id', $invoice->id)
->where('group_name', $data['group_name']) ->where('group_name', $data['groupname'])
->exists(); ->exists();
if ($exists) { if ($exists) {
return back() return back()
->withErrors(['group_name' => 'This group name is already used for this invoice.']) ->withErrors(['groupname' => 'This group name is already used for this invoice.'])
->withInput(); ->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([ $group = InvoiceChargeGroup::create([
'invoice_id' => $invoice->id, 'invoice_id' => $invoice->id,
'group_name' => $data['group_name'], 'group_name' => $data['groupname'],
'basis_type' => $data['basis_type'], 'basis_type' => $data['basistype'],
'basis_value' => $data['basis_value'], 'basis_value' => $data['basisvalue'],
'rate' => $data['rate'], 'rate' => $data['rate'],
'total_charge' => $data['auto_total'], 'total_charge' => $baseTotal,
'tax_type' => $taxType,
'gst_percent' => $gstPercent,
'total_with_gst' => $totalWithGst,
]); ]);
foreach ($data['item_ids'] as $itemId) { foreach ($data['itemids'] as $itemId) {
InvoiceChargeGroupItem::create([ InvoiceChargeGroupItem::create([
'group_id' => $group->id, 'group_id' => $group->id,
'invoice_item_id' => $itemId, 'invoice_item_id' => $itemId,
]); ]);
} }
// ⭐ Charge groups नुसार Total Charges सेट करा $invoice->load('chargeGroups');
$chargeGroupsTotal = $invoice->chargeGroups()->sum('total_charge');
$grandTotal = $invoice->final_amount_with_gst + $chargeGroupsTotal;
$invoice->update([ $chargeGroupsBase = $invoice->chargeGroups->sum('total_charge');
'charge_groups_total' => $chargeGroupsTotal, $chargeGroupsWithG = $invoice->chargeGroups->sum('total_with_gst');
'grand_total_with_charges' => $grandTotal, $chargeGroupsGst = $chargeGroupsWithG - $chargeGroupsBase;
]);
return redirect() $invoiceGstPercent = $group->gst_percent ?? 0;
->back() $invoiceTaxType = $group->tax_type ?? 'gst';
->with('success', 'Charge group saved successfully.');
}
public function download(Invoice $invoice) $cgstPercent = 0;
{ $sgstPercent = 0;
if (!$invoice->pdf_path || !Storage::exists($invoice->pdf_path)) { $igstPercent = 0;
return back()->with('error', 'PDF not found.');
if ($invoiceTaxType === 'gst') {
$cgstPercent = $invoiceGstPercent / 2;
$sgstPercent = $invoiceGstPercent / 2;
} elseif ($invoiceTaxType === 'igst') {
$igstPercent = $invoiceGstPercent;
} }
return Storage::download($invoice->pdf_path, $invoice->invoice_number . '.pdf'); $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));
}
} }

View File

@@ -28,11 +28,23 @@ class AdminOrderController extends Controller
* ---------------------------*/ * ---------------------------*/
public function dashboard() public function dashboard()
{ {
$totalOrders = Order::count(); // ── Order counts (from Invoice Management / Orders table) ──
$pendingOrders = Order::where('status', 'pending')->count(); $totalOrders = Order::count();
$totalShipments = Shipment::count();
$totalItems = OrderItem::count(); // "Pending" म्हणजे delivered नसलेले सर्व orders
// Order तयार होतो तेव्हा status = 'order_placed' असतो, 'pending' नाही
$deliveredStatuses = ['delivered'];
$pendingOrders = Order::whereNotIn('status', $deliveredStatuses)->count();
// ── Invoice counts ──
$totalContainers = Container::count();
$totalInvoices = Invoice::count();
$paidInvoices = Invoice::where('status', 'paid')->count();
$pendingInvoices = Invoice::where('status', 'pending')->count();
$overdueInvoices = Invoice::where('status', 'overdue')->count();
$totalRevenue = Invoice::sum('final_amount_with_gst'); $totalRevenue = Invoice::sum('final_amount_with_gst');
// ── User / Staff counts ──
$activeCustomers = User::where('status', 'active')->count(); $activeCustomers = User::where('status', 'active')->count();
$inactiveCustomers = User::where('status', 'inactive')->count(); $inactiveCustomers = User::where('status', 'inactive')->count();
$totalStaff = Admin::where('type', 'staff')->count(); $totalStaff = Admin::where('type', 'staff')->count();
@@ -43,8 +55,11 @@ class AdminOrderController extends Controller
return view('admin.dashboard', compact( return view('admin.dashboard', compact(
'totalOrders', 'totalOrders',
'pendingOrders', 'pendingOrders',
'totalShipments', 'totalContainers',
'totalItems', 'totalInvoices',
'paidInvoices',
'pendingInvoices',
'overdueInvoices',
'totalRevenue', 'totalRevenue',
'activeCustomers', 'activeCustomers',
'inactiveCustomers', 'inactiveCustomers',
@@ -90,11 +105,23 @@ class AdminOrderController extends Controller
->when($request->filled('status'), function ($q) use ($request) { ->when($request->filled('status'), function ($q) use ($request) {
$q->where('invoices.status', $request->status); $q->where('invoices.status', $request->status);
}) })
->orderByDesc('invoices.invoice_date') // इथे बदल ->orderByDesc('invoices.invoice_date')
->orderByDesc('invoices.id') // same-date साठी tie-breaker ->orderByDesc('invoices.id')
->get(); ->get();
return view('admin.orders', compact('invoices')); // ── Real DB counts (filter-independent) ──
$totalInvoices = \App\Models\Invoice::count();
$paidInvoices = \App\Models\Invoice::where('status', 'paid')->count();
$pendingInvoices = \App\Models\Invoice::where('status', 'pending')->count();
$overdueInvoices = \App\Models\Invoice::where('status', 'overdue')->count();
return view('admin.orders', compact(
'invoices',
'totalInvoices',
'paidInvoices',
'pendingInvoices',
'overdueInvoices'
));
} }
/* --------------------------- /* ---------------------------

View File

@@ -5,104 +5,197 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
use Mpdf\Mpdf;
class AdminReportController extends Controller class AdminReportController extends Controller
{ {
/** // UI साठी main action
* Display the reports page with joined data public function containerReport(Request $request)
*/
// public function index(Request $request)
// {
/*********************************************************
* OLD FLOW (Order + Shipment + Invoice)
* फक्त reference साठी ठेवलेला, वापरत नाही.
*********************************************************/
/*
$reports = DB::table('orders')
->join('shipment_items', 'shipment_items.order_id', '=', 'orders.id')
->join('shipments', 'shipments.id', '=', 'shipment_items.shipment_id')
->join('invoices', 'invoices.order_id', '=', 'orders.id')
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'orders.mark_no')
->leftJoin('users', 'users.customer_id', '=', 'mark_list.customer_id')
->select(...)
->orderBy('shipments.shipment_date', 'desc')
->get();
*/
/*********************************************************
* NEW FLOW (Container + Invoice + MarkList)
*********************************************************/
// $reports = DB::table('invoices')
// ->join('containers', 'containers.id', '=', 'invoices.containerid')
// ->leftJoin('mark_list', 'mark_list.markno', '=', 'invoices.markno')
// ->select(
// 'invoices.id as invoicepk',
// 'invoices.invoicenumber',
// 'invoices.invoicedate',
// 'invoices.finalamount',
// 'invoices.finalamountwithgst',
// 'invoices.gstpercent',
// 'invoices.gstamount',
// 'invoices.status as invoicestatus',
// 'invoices.markno',
// 'containers.id as containerpk',
// 'containers.containernumber',
// 'containers.containerdate',
// 'containers.containername',
// 'mark_list.companyname',
// 'mark_list.customername'
// )
// ->orderBy('containers.containerdate', 'desc')
// ->get();
// return view('admin.reports', compact('reports'));
// }
public function index(Request $request)
{ {
$reports = DB::table('invoices') $reports = $this->buildContainerReportQuery($request)->get();
->join('containers', 'containers.id', '=', 'invoices.container_id')
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
->select(
// INVOICE
'invoices.id as invoicepk',
'invoices.invoice_number',
'invoices.invoice_date',
'invoices.final_amount',
'invoices.final_amount_with_gst',
'invoices.gst_percent',
'invoices.gst_amount',
'invoices.status as invoicestatus',
'invoices.mark_no',
// CONTAINER
'containers.id as containerpk',
'containers.container_number',
'containers.container_date',
'containers.container_name',
// RAW FIELDS (for reference/debug if needed)
'invoices.company_name as inv_company_name',
'invoices.customer_name as inv_customer_name',
'mark_list.company_name as ml_company_name',
'mark_list.customer_name as ml_customer_name',
// FINAL FIELDS (automatically pick invoice first, else mark_list)
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name')
)
->orderBy('invoices.invoice_date', 'desc')
->orderBy('invoices.id', 'desc')
->get();
return view('admin.reports', compact('reports')); 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 . '"',
]);
}
} }

View File

@@ -11,7 +11,7 @@ use Illuminate\Http\Request;
use Maatwebsite\Excel\Facades\Excel; use Maatwebsite\Excel\Facades\Excel;
use Carbon\Carbon; use Carbon\Carbon;
use Barryvdh\DomPDF\Facade\Pdf; use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\Storage; // <-- added for Excel download use Illuminate\Support\Facades\Storage;
class ContainerController extends Controller class ContainerController extends Controller
{ {
@@ -193,6 +193,7 @@ class ContainerController extends Controller
'kg_col' => null, 'kg_col' => null,
'totalkg_col' => null, 'totalkg_col' => null,
'itemno_col' => null, 'itemno_col' => null,
'shopno_col' => null,
]; ];
foreach ($header as $colIndex => $headingText) { foreach ($header as $colIndex => $headingText) {
@@ -233,6 +234,11 @@ class ContainerController extends Controller
strpos($normalized, 'ITEM') !== false strpos($normalized, 'ITEM') !== false
) { ) {
$essentialColumns['itemno_col'] = $colIndex; $essentialColumns['itemno_col'] = $colIndex;
} elseif (
strpos($normalized, 'SHOPNO') !== false ||
strpos($normalized, 'SHOP') !== false
) {
$essentialColumns['shopno_col'] = $colIndex;
} }
} }
@@ -395,8 +401,8 @@ class ContainerController extends Controller
if (!empty($unmatchedMarks)) { if (!empty($unmatchedMarks)) {
foreach ($cleanedRows as $item) { foreach ($cleanedRows as $item) {
$row = $item['row']; $row = $item['row'];
$offset = $item['offset']; $offset = $item['offset'];
$rowMark = trim((string)($row[$essentialColumns['itemno_col']] ?? '')); $rowMark = trim((string)($row[$essentialColumns['itemno_col']] ?? ''));
if ($rowMark === '' || !in_array($rowMark, $unmatchedMarks)) { if ($rowMark === '' || !in_array($rowMark, $unmatchedMarks)) {
@@ -518,17 +524,17 @@ class ContainerController extends Controller
$firstMark = $rowsForCustomer[0]['mark']; $firstMark = $rowsForCustomer[0]['mark'];
$snap = $markToSnapshot[$firstMark] ?? null; $snap = $markToSnapshot[$firstMark] ?? null;
$customerUser = \App\Models\User::where('customer_id', $customerId)->first();
$invoice = new Invoice(); $invoice = new Invoice();
$invoice->container_id = $container->id; $invoice->container_id = $container->id;
// $invoice->customer_id = $customerId; $invoice->customer_id = $customerUser->id ?? null;
$invoice->mark_no = $firstMark; $invoice->mark_no = $firstMark;
$invoice->invoice_number = $this->generateInvoiceNumber(); $invoice->invoice_number = $this->generateInvoiceNumber();
// invoice_date = container_date
$invoice->invoice_date = $container->container_date; $invoice->invoice_date = $container->container_date;
// due_date = container_date + 10 days
$invoice->due_date = Carbon::parse($invoice->invoice_date) $invoice->due_date = Carbon::parse($invoice->invoice_date)
->addDays(10) ->addDays(10)
->format('Y-m-d'); ->format('Y-m-d');
@@ -539,15 +545,17 @@ class ContainerController extends Controller
$invoice->customer_mobile = $snap['mobile_no'] ?? 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->final_amount = 0;
$invoice->gst_percent = 0; $invoice->gst_percent = 0;
$invoice->gst_amount = 0; $invoice->gst_amount = 0;
$invoice->final_amount_with_gst = 0; $invoice->final_amount_with_gst = 0;
$invoice->customer_email = null;
$invoice->customer_address = null;
$invoice->pincode = null;
$uniqueMarks = array_unique(array_column($rowsForCustomer, 'mark')); $uniqueMarks = array_unique(array_column($rowsForCustomer, 'mark'));
$invoice->notes = 'Auto-created from Container ' . $container->container_number $invoice->notes = 'Auto-created from Container ' . $container->container_number
. ' for Mark(s): ' . implode(', ', $uniqueMarks); . ' for Mark(s): ' . implode(', ', $uniqueMarks);
@@ -557,11 +565,10 @@ class ContainerController extends Controller
$invoice->save(); $invoice->save();
$invoiceCount++; $invoiceCount++;
$totalAmount = 0;
foreach ($rowsForCustomer as $item) { foreach ($rowsForCustomer as $item) {
$row = $item['row']; $row = $item['row'];
$offset = $item['offset']; $offset = $item['offset'];
$mark = $item['mark']; // ✅ mark_no from Excel
$description = $essentialColumns['desc_col'] !== null ? ($row[$essentialColumns['desc_col']] ?? null) : null; $description = $essentialColumns['desc_col'] !== null ? ($row[$essentialColumns['desc_col']] ?? null) : null;
$ctn = $essentialColumns['ctn_col'] !== null ? (int) ($row[$essentialColumns['ctn_col']] ?? 0) : 0; $ctn = $essentialColumns['ctn_col'] !== null ? (int) ($row[$essentialColumns['ctn_col']] ?? 0) : 0;
@@ -575,6 +582,8 @@ class ContainerController extends Controller
$kg = $essentialColumns['kg_col'] !== null ? (float) ($row[$essentialColumns['kg_col']] ?? 0) : 0; $kg = $essentialColumns['kg_col'] !== null ? (float) ($row[$essentialColumns['kg_col']] ?? 0) : 0;
$ttlKg = $essentialColumns['totalkg_col'] !== null ? (float) ($row[$essentialColumns['totalkg_col']] ?? $kg) : $kg; $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; $rowIndex = $headerRowIndex + 1 + $offset;
InvoiceItem::create([ InvoiceItem::create([
@@ -592,18 +601,10 @@ class ContainerController extends Controller
'ttl_cbm' => $ttlCbm, 'ttl_cbm' => $ttlCbm,
'kg' => $kg, 'kg' => $kg,
'ttl_kg' => $ttlKg, 'ttl_kg' => $ttlKg,
'shop_no' => null, 'shop_no' => $shopNo,
'mark_no' => $mark, // ✅ save mark_no from Excel
]); ]);
$totalAmount += $ttlAmount;
} }
$invoice->final_amount = $totalAmount;
$invoice->gst_percent = 0;
$invoice->gst_amount = 0;
$invoice->final_amount_with_gst = $totalAmount;
$invoice->save();
} }
$msg = "Container '{$container->container_number}' created with {$savedCount} rows and {$invoiceCount} customer invoice(s)."; $msg = "Container '{$container->container_number}' created with {$savedCount} rows and {$invoiceCount} customer invoice(s).";
@@ -613,7 +614,17 @@ class ContainerController extends Controller
public function show(Container $container) public function show(Container $container)
{ {
$container->load('rows'); $container->load('rows');
return view('admin.container_show', compact('container'));
$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) public function updateRows(Request $request, Container $container)
@@ -629,14 +640,12 @@ class ContainerController extends Controller
continue; continue;
} }
// 1) update container_rows.data
$data = $row->data ?? []; $data = $row->data ?? [];
foreach ($cols as $colHeader => $value) { foreach ($cols as $colHeader => $value) {
$data[$colHeader] = $value; $data[$colHeader] = $value;
} }
$row->update(['data' => $data]); $row->update(['data' => $data]);
// 2) normalize keys
$normalizedMap = []; $normalizedMap = [];
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
if ($key === null || $key === '') { if ($key === null || $key === '') {
@@ -648,7 +657,6 @@ class ContainerController extends Controller
$normalizedMap[$normKey] = $value; $normalizedMap[$normKey] = $value;
} }
// helper: get first numeric value from given keys
$getFirstNumeric = function (array $map, array $possibleKeys) { $getFirstNumeric = function (array $map, array $possibleKeys) {
foreach ($possibleKeys as $search) { foreach ($possibleKeys as $search) {
$normSearch = strtoupper($search); $normSearch = strtoupper($search);
@@ -668,23 +676,19 @@ class ContainerController extends Controller
return 0; return 0;
}; };
// 3) read values QTY vs TTLQTY separately
$ctnKeys = ['CTN', 'CTNS']; $ctnKeys = ['CTN', 'CTNS'];
$qtyKeys = ['QTY', 'PCS', 'PIECES']; // per-carton qty $qtyKeys = ['QTY', 'PCS', 'PIECES'];
$ttlQtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY']; // total qty $ttlQtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY'];
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM']; $cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT']; $kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
$amountKeys = ['AMOUNT', 'TTLAMOUNT', 'TOTALAMOUNT']; $amountKeys = ['AMOUNT', 'TTLAMOUNT', 'TOTALAMOUNT'];
$ctn = $getFirstNumeric($normalizedMap, $ctnKeys); $ctn = $getFirstNumeric($normalizedMap, $ctnKeys);
// per carton qty
$qty = $getFirstNumeric($normalizedMap, $qtyKeys); $qty = $getFirstNumeric($normalizedMap, $qtyKeys);
// total qty direct from TOTALQTY/TTLQTY/ITLQTY
$ttlQ = $getFirstNumeric($normalizedMap, $ttlQtyKeys); $ttlQ = $getFirstNumeric($normalizedMap, $ttlQtyKeys);
// if total column is 0 then compute ctn * qty
if ($ttlQ == 0 && $ctn && $qty) { if ($ttlQ == 0 && $ctn && $qty) {
$ttlQ = $ctn * $qty; $ttlQ = $ctn * $qty;
} }
@@ -707,7 +711,6 @@ class ContainerController extends Controller
$amount = $price * $ttlQ; $amount = $price * $ttlQ;
} }
// 4) get description
$desc = null; $desc = null;
foreach (['DESCRIPTION', 'DESC'] as $dKey) { foreach (['DESCRIPTION', 'DESC'] as $dKey) {
$normD = str_replace([' ', '/', '-', '.'], '', strtoupper($dKey)); $normD = str_replace([' ', '/', '-', '.'], '', strtoupper($dKey));
@@ -719,9 +722,31 @@ class ContainerController extends Controller
} }
} }
$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; $rowIndex = $row->row_index;
// 5) find linked invoice_items
$items = InvoiceItem::where('container_id', $container->id) $items = InvoiceItem::where('container_id', $container->id)
->where('container_row_index', $rowIndex) ->where('container_row_index', $rowIndex)
->get(); ->get();
@@ -733,18 +758,19 @@ class ContainerController extends Controller
->get(); ->get();
} }
// 6) update invoice_items + recalc invoice totals
foreach ($items as $item) { foreach ($items as $item) {
$item->description = $desc; $item->description = $desc;
$item->ctn = $ctn; $item->ctn = $ctn;
$item->qty = $qty; // per carton $item->qty = $qty;
$item->ttl_qty = $ttlQ; // total $item->ttl_qty = $ttlQ;
$item->price = $price; $item->price = $price;
$item->ttl_amount = $amount; $item->ttl_amount = $amount;
$item->cbm = $cbm; $item->cbm = $cbm;
$item->ttl_cbm = $ttlC; $item->ttl_cbm = $ttlC;
$item->kg = $kg; $item->kg = $kg;
$item->ttl_kg = $ttlK; $item->ttl_kg = $ttlK;
$item->shop_no = $shopNo;
$item->mark_no = $markNo; // ✅ update mark_no
$item->save(); $item->save();
$invoice = $item->invoice; $invoice = $item->invoice;
@@ -859,7 +885,6 @@ class ContainerController extends Controller
abort(404, 'Excel file not found on record.'); abort(404, 'Excel file not found on record.');
} }
// Stored path like "containers/abc.xlsx"
$path = $container->excel_file; $path = $container->excel_file;
if (!Storage::exists($path)) { if (!Storage::exists($path)) {
@@ -871,73 +896,69 @@ class ContainerController extends Controller
return Storage::download($path, $fileName); return Storage::download($path, $fileName);
} }
public function popupPopup(Container $container) public function popupPopup(Container $container)
{ {
// existing show सारखाच data वापरू $container->load('rows');
$container->load('rows');
// summary आधीपासून index() मध्ये जसा काढतोस तसाच logic reuse $rows = $container->rows ?? collect();
$rows = $container->rows ?? collect();
$totalCtn = 0; $totalCtn = 0;
$totalQty = 0; $totalQty = 0;
$totalCbm = 0; $totalCbm = 0;
$totalKg = 0; $totalKg = 0;
$ctnKeys = ['CTN', 'CTNS']; $ctnKeys = ['CTN', 'CTNS'];
$qtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY', 'QTY', 'PCS', 'PIECES']; $qtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY', 'QTY', 'PCS', 'PIECES'];
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM']; $cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT']; $kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
$getFirstNumeric = function (array $data, array $possibleKeys) { $getFirstNumeric = function (array $data, array $possibleKeys) {
$normalizedMap = []; $normalizedMap = [];
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
if ($key === null) continue; if ($key === null) continue;
$normKey = strtoupper((string)$key); $normKey = strtoupper((string)$key);
$normKey = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normKey); $normKey = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normKey);
$normalizedMap[$normKey] = $value; $normalizedMap[$normKey] = $value;
} }
foreach ($possibleKeys as $search) { foreach ($possibleKeys as $search) {
$normSearch = strtoupper($search); $normSearch = strtoupper($search);
$normSearch = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normSearch); $normSearch = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normSearch);
foreach ($normalizedMap as $nKey => $value) { foreach ($normalizedMap as $nKey => $value) {
if (strpos($nKey, $normSearch) !== false) { if (strpos($nKey, $normSearch) !== false) {
if (is_numeric($value)) { if (is_numeric($value)) {
return (float)$value; return (float)$value;
} }
if (is_string($value) && is_numeric(trim($value))) { if (is_string($value) && is_numeric(trim($value))) {
return (float)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);
} }
return 0;
};
foreach ($rows as $row) { $summary = [
$data = $row->data ?? []; 'total_ctn' => round($totalCtn, 2),
if (!is_array($data)) continue; 'total_qty' => round($totalQty, 2),
'total_cbm' => round($totalCbm, 3),
'total_kg' => round($totalKg, 2),
];
$totalCtn += $getFirstNumeric($data, $ctnKeys); return view('admin.partials.container_popup_readonly', [
$totalQty += $getFirstNumeric($data, $qtyKeys); 'container' => $container,
$totalCbm += $getFirstNumeric($data, $cbmKeys); 'summary' => $summary,
$totalKg += $getFirstNumeric($data, $kgKeys); ]);
} }
$summary = [
'total_ctn' => round($totalCtn, 2),
'total_qty' => round($totalQty, 2),
'total_cbm' => round($totalCbm, 3),
'total_kg' => round($totalKg, 2),
];
return view('admin.partials.container_popup_readonly', [
'container' => $container,
'summary' => $summary,
]);
}
} }

View File

@@ -21,23 +21,33 @@ class UserOrderController extends Controller
} }
// ------------------------------------- // -------------------------------------
// Get all orders // Get customer invoices with containers
// ------------------------------------- // -------------------------------------
$orders = $user->orders()->with('invoice')->get(); $invoices = $user->invoices()->with('container')->get();
// Unique containers for this customer
$containers = $invoices->pluck('container')->filter()->unique('id');
// ------------------------------------- // -------------------------------------
// Counts // Counts based on container status
// ------------------------------------- // -------------------------------------
$totalOrders = $orders->count(); $totalOrders = $containers->count();
$delivered = $orders->where('status', 'delivered')->count();
$inTransit = $orders->where('status', '!=', 'delivered')->count(); $delivered = $containers->where('status', 'delivered')->count();
$active = $totalOrders;
$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) { $totalAmount = $invoices->sum(function ($invoice) {
return $o->invoice->final_amount_with_gst ?? 0; return $invoice->final_amount_with_gst ?? 0;
}); });
// Format total amount in K, L, Cr // Format total amount in K, L, Cr
@@ -45,13 +55,12 @@ class UserOrderController extends Controller
return response()->json([ return response()->json([
'status' => true, 'status' => true,
'summary' => [ 'summary' => [
'active_orders' => $active, 'active_orders' => $active,
'in_transit_orders' => $inTransit, 'in_transit_orders' => $inTransit,
'delivered_orders' => $delivered, 'delivered_orders' => $delivered,
'total_value' => $formattedAmount, // formatted value 'total_value' => $formattedAmount,
'total_raw' => $totalAmount // original value 'total_raw' => $totalAmount
] ]
]); ]);
} }
@@ -90,20 +99,28 @@ class UserOrderController extends Controller
], 401); ], 401);
} }
// Fetch orders for this user // Get invoices with containers for this customer
$orders = $user->orders() $invoices = $user->invoices()
->with(['invoice', 'shipments']) ->with('container')
->orderBy('id', 'desc') ->orderBy('id', 'desc')
->get() ->get();
->map(function ($o) {
return [ // Extract unique containers
'order_id' => $o->order_id, $containers = $invoices->pluck('container')
'status' => $o->status, ->filter()
'amount' => $o->ttl_amount, ->unique('id')
'description'=> "Order from {$o->origin} to {$o->destination}", ->values();
'created_at' => $o->created_at,
]; $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([ return response()->json([
'success' => true, 'success' => true,
@@ -115,45 +132,73 @@ public function orderDetails($order_id)
{ {
$user = JWTAuth::parseToken()->authenticate(); $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']) ->with(['items'])
->where('order_id', $order_id)
->first(); ->first();
if (!$order) { if (!$invoice) {
return response()->json(['success' => false, 'message' => 'Order not found'], 404); return response()->json([
'success' => false,
'message' => 'Order not found for this user'
], 404);
} }
return response()->json([ return response()->json([
'success' => true, '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) // public function orderShipment($order_id)
{ // {
$user = JWTAuth::parseToken()->authenticate(); // $user = JWTAuth::parseToken()->authenticate();
// Get order // // Get order
$order = $user->orders()->where('order_id', $order_id)->first(); // $order = $user->orders()->where('order_id', $order_id)->first();
if (!$order) { // if (!$order) {
return response()->json(['success' => false, 'message' => 'Order not found'], 404); // return response()->json(['success' => false, 'message' => 'Order not found'], 404);
} // }
// Find shipment only for this order // // Find shipment only for this order
$shipment = $order->shipments() // $shipment = $order->shipments()
->with(['items' => function ($q) use ($order) { // ->with(['items' => function ($q) use ($order) {
$q->where('order_id', $order->id); // $q->where('order_id', $order->id);
}]) // }])
->first(); // ->first();
return response()->json([ // return response()->json([
'success' => true, // 'success' => true,
'shipment' => $shipment // 'shipment' => $shipment
]); // ]);
} // }
public function orderInvoice($order_id) public function orderInvoice($order_id)
@@ -179,23 +224,35 @@ public function trackOrder($order_id)
{ {
$user = JWTAuth::parseToken()->authenticate(); $user = JWTAuth::parseToken()->authenticate();
$order = $user->orders() if (!$user) {
->with('shipments') return response()->json([
->where('order_id', $order_id) 'success' => false,
->first(); 'message' => 'Unauthorized'
], 401);
if (!$order) {
return response()->json(['success' => false, 'message' => 'Order not found'], 404);
} }
$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([ return response()->json([
'success' => true, 'success' => true,
'track' => [ 'track' => [
'order_id' => $order->order_id, 'order_id' => $container->id,
'shipment_status' => $shipment->status ?? 'pending', 'container_number' => $container->container_number,
'shipment_date' => $shipment->shipment_date ?? null, 'status' => $container->status,
'container_date' => $container->container_date,
] ]
]); ]);
} }
@@ -289,44 +346,44 @@ public function invoiceDetails($invoice_id)
]); ]);
} }
public function confirmOrder($order_id) // public function confirmOrder($order_id)
{ // {
$user = JWTAuth::parseToken()->authenticate(); // $user = JWTAuth::parseToken()->authenticate();
if (! $user) { // if (! $user) {
return response()->json([ // return response()->json([
'success' => false, // 'success' => false,
'message' => 'Unauthorized' // 'message' => 'Unauthorized'
], 401); // ], 401);
} // }
$order = $user->orders() // $order = $user->orders()
->where('order_id', $order_id) // ->where('order_id', $order_id)
->first(); // ->first();
if (! $order) { // if (! $order) {
return response()->json([ // return response()->json([
'success' => false, // 'success' => false,
'message' => 'Order not found' // 'message' => 'Order not found'
], 404); // ], 404);
} // }
// 🚫 Only allow confirm from order_placed // // 🚫 Only allow confirm from order_placed
if ($order->status !== 'order_placed') { // if ($order->status !== 'order_placed') {
return response()->json([ // return response()->json([
'success' => false, // 'success' => false,
'message' => 'Order cannot be confirmed' // 'message' => 'Order cannot be confirmed'
], 422); // ], 422);
} // }
$order->status = 'order_confirmed'; // $order->status = 'order_confirmed';
$order->save(); // $order->save();
return response()->json([ // return response()->json([
'success' => true, // 'success' => true,
'message' => 'Order confirmed successfully' // 'message' => 'Order confirmed successfully'
]); // ]);
} // }

View File

@@ -31,6 +31,13 @@ class Invoice extends Model
'pincode', 'pincode',
'pdf_path', 'pdf_path',
'notes', 'notes',
// totals from charge groups
'charge_groups_total',
'grand_total_with_charges',
'tax_type',
'cgst_percent',
'sgst_percent',
'igst_percent',
]; ];
/**************************** /****************************
@@ -42,10 +49,10 @@ class Invoice extends Model
return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC'); return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC');
} }
public function container() // public function container()
{ // {
return $this->belongsTo(Container::class); // return $this->belongsTo(Container::class);
} // }
public function customer() public function customer()
{ {
@@ -57,16 +64,16 @@ class Invoice extends Model
return $this->hasMany(InvoiceInstallment::class); return $this->hasMany(InvoiceInstallment::class);
} }
// ✅ SINGLE, correct relation
public function chargeGroups() public function chargeGroups()
{ {
return $this->hasMany(\App\Models\InvoiceChargeGroup::class, 'invoice_id'); return $this->hasMany(InvoiceChargeGroup::class, 'invoice_id');
} }
/**************************** /****************************
* Helper Functions * Helper Functions
****************************/ ****************************/
// (Items based calculateTotals वापरणार नाहीस तरी ठेवू शकतोस)
public function calculateTotals() public function calculateTotals()
{ {
$gst = ($this->final_amount * $this->gst_percent) / 100; $gst = ($this->final_amount * $this->gst_percent) / 100;
@@ -84,29 +91,43 @@ class Invoice extends Model
return null; return null;
} }
// ✅ Charge groups total accessor // ✅ Charge groups base total (WITHOUT GST)
public function getChargeGroupsTotalAttribute() public function getChargeGroupsTotalAttribute()
{ {
// relation already loaded असेल तर collection वरून sum होईल // base = total_charge sum
return (float) $this->chargeGroups->sum('total_charge'); return (float) $this->chargeGroups->sum('total_charge');
} }
// ✅ Grand total accessor (items + GST + charge groups) // ✅ Grand total: Charge groups base + GST (items ignore)
public function getGrandTotalWithChargesAttribute() public function getGrandTotalWithChargesAttribute()
{ {
return (float) ($this->final_amount_with_gst ?? 0) + $this->charge_groups_total; $base = (float) ($this->charge_groups_total ?? 0);
$gst = (float) ($this->gst_amount ?? 0);
return $base + $gst;
} }
public function totalPaid() 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->installments->sum('amount'); return $this->belongsTo(\App\Models\Container::class, 'container_id');
} }
public function remainingAmount()
{
return $this->grand_total_with_charges - $this->totalPaid();
}
} }

View File

@@ -13,6 +13,10 @@ class InvoiceChargeGroup extends Model
'basis_value', 'basis_value',
'rate', 'rate',
'total_charge', 'total_charge',
'tax_type',
'gst_percent',
'total_with_gst',
]; ];
public function invoice() public function invoice()

View File

@@ -29,6 +29,7 @@ class InvoiceItem extends Model
'ttl_kg', 'ttl_kg',
'shop_no', 'shop_no',
'mark_no',
]; ];
/**************************** /****************************

View File

@@ -89,10 +89,7 @@ class User extends Authenticatable implements JWTSubject
{ {
return []; return [];
} }
public function invoices()
{
return $this->hasMany(\App\Models\Invoice::class, 'customer_id', 'customer_id');
}
// App\Models\User.php // App\Models\User.php
@@ -108,6 +105,10 @@ public function invoiceInstallments()
); );
} }
public function invoices()
{
return $this->hasMany(\App\Models\Invoice::class, 'customer_id', 'id');
}
} }

View File

@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('invoice_charge_groups', function (Blueprint $table) {
$table->string('tax_type')->nullable()->after('total_charge');
$table->decimal('gst_percent', 5, 2)->default(0)->after('tax_type');
$table->decimal('total_with_gst', 15, 2)->default(0)->after('gst_percent');
});
}
public function down(): void
{
Schema::table('invoice_charge_groups', function (Blueprint $table) {
$table->dropColumn(['tax_type', 'gst_percent', 'total_with_gst']);
});
}
};

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddChargeColumnsToInvoicesTable extends Migration
{
public function up()
{
Schema::table('invoices', function (Blueprint $table) {
if (!Schema::hasColumn('invoices', 'charge_groups_total')) {
$table->decimal('charge_groups_total', 15, 2)
->nullable()
->after('final_amount_with_gst');
}
if (!Schema::hasColumn('invoices', 'grand_total_with_charges')) {
$table->decimal('grand_total_with_charges', 15, 2)
->nullable()
->after('charge_groups_total');
}
});
}
public function down()
{
Schema::table('invoices', function (Blueprint $table) {
if (Schema::hasColumn('invoices', 'charge_groups_total')) {
$table->dropColumn('charge_groups_total');
}
if (Schema::hasColumn('invoices', 'grand_total_with_charges')) {
$table->dropColumn('grand_total_with_charges');
}
});
}
}

View File

@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration {
public function up(): void
{
DB::statement("
ALTER TABLE `invoices`
MODIFY `status` ENUM('pending','paying','paid','overdue')
NOT NULL DEFAULT 'pending'
");
}
public function down(): void
{
DB::statement("
ALTER TABLE `invoices`
MODIFY `status` ENUM('pending','paid','overdue')
NOT NULL DEFAULT 'pending'
");
}
};

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::table('invoice_items', function (Blueprint $table) {
$table->string('mark_no')->nullable(); // after() काहीही नको
});
}
public function down()
{
Schema::table('invoice_items', function (Blueprint $table) {
$table->dropColumn('mark_no');
});
}
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -40,6 +40,72 @@
margin-top: 2px; 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 { .cm-main-card {
border-radius: 14px; border-radius: 14px;
border: none; border: none;
@@ -65,7 +131,7 @@
.cm-info-cards-row { .cm-info-cards-row {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); /* 3 cards one row */ grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px; gap: 14px;
padding: 14px 18px 8px 18px; padding: 14px 18px 8px 18px;
} }
@@ -132,7 +198,6 @@
color: #fff7ed; color: #fff7ed;
} }
/* TOTAL BOXES */
.cm-total-cards-row { .cm-total-cards-row {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -221,7 +286,7 @@
} }
.cm-table-scroll-outer { .cm-table-scroll-outer {
margin: 10px 14px 0 14px; margin: 10px 14px 30px 14px;
border-radius: 14px; border-radius: 14px;
border: 1.5px solid #c9a359; border: 1.5px solid #c9a359;
box-shadow: 0 4px 14px rgba(76,111,255,0.18); box-shadow: 0 4px 14px rgba(76,111,255,0.18);
@@ -433,15 +498,18 @@
</div> </div>
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a href="{{ route('containers.index') }}" class="btn btn-light btn-sm">Back to list</a> <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) }}" <a href="{{ route('containers.download.pdf', $container->id) }}" class="cm-download-pdf">
class="btn btn-sm btn-outline-primary"> <i class="bi bi-file-earmark-pdf"></i>
Download PDF Download PDF
</a> </a>
<a href="{{ route('containers.download.excel', $container->id) }}" <a href="{{ route('containers.download.excel', $container->id) }}" class="cm-download-excel">
class="btn btn-sm btn-outline-success"> <i class="bi bi-file-earmark-excel"></i>
Download Excel Download Excel
</a> </a>
</div> </div>
@@ -453,7 +521,6 @@
<h5>Container Information</h5> <h5>Container Information</h5>
</div> </div>
{{-- 3 INFO CARDS IN SINGLE ROW --}}
<div class="cm-info-cards-row"> <div class="cm-info-cards-row">
<div class="cm-info-card cm-card-container"> <div class="cm-info-card cm-card-container">
<div class="cm-info-card-icon"> <div class="cm-info-card-icon">
@@ -608,7 +675,7 @@
<div class="cm-filter-bar"> <div class="cm-filter-bar">
<span class="cm-row-count"> <span class="cm-row-count">
Total rows: {{ $container->rows->count() }} &nbsp;&nbsp; Edit cells then click "Save Changes". Total rows: {{ $container->rows->count() }} Edit cells then click "Save Changes".
</span> </span>
<input type="text" id="cmRowSearch" class="cm-filter-input" <input type="text" id="cmRowSearch" class="cm-filter-input"
placeholder="Quick search..." onkeyup="cmFilterRows()"> placeholder="Quick search..." onkeyup="cmFilterRows()">
@@ -666,17 +733,20 @@
$isPrice = (str_contains($norm, 'PRICE') || str_contains($norm, 'RATE')); $isPrice = (str_contains($norm, 'PRICE') || str_contains($norm, 'RATE'));
$isAmount = ( $isAmount = (
str_contains($norm, 'AMOUNT') || str_contains($norm, 'AMOUNT') ||
str_contains($norm, 'TTLAMOUNT') || str_contains($norm, 'TTLAMOUNT') ||
str_contains($norm, 'TOTALAMOUNT') str_contains($norm, 'TOTALAMOUNT')
); );
$isTotalColumn = $isTotalQty || $isTotalCbm || $isTotalKg || $isAmount; $isTotalColumn = $isTotalQty || $isTotalCbm || $isTotalKg || $isAmount;
$isLockedByInvoice = in_array($row->row_index, $lockedRowIndexes ?? []);
$isReadOnly = $isTotalColumn || $container->status !== 'pending' || $isLockedByInvoice;
@endphp @endphp
<td> <td>
<input <input
type="text" type="text"
class="cm-cell-input {{ $isTotalColumn ? 'cm-cell-readonly' : '' }}" class="cm-cell-input {{ $isReadOnly ? 'cm-cell-readonly' : '' }}"
name="rows[{{ $row->id }}][{{ $heading }}]" name="rows[{{ $row->id }}][{{ $heading }}]"
value="{{ $value }}" value="{{ $value }}"
data-row-id="{{ $row->id }}" data-row-id="{{ $row->id }}"
@@ -690,7 +760,7 @@
data-ttlkg="{{ $isTotalKg ? '1' : '0' }}" data-ttlkg="{{ $isTotalKg ? '1' : '0' }}"
data-price="{{ $isPrice ? '1' : '0' }}" data-price="{{ $isPrice ? '1' : '0' }}"
data-amount="{{ $isAmount ? '1' : '0' }}" data-amount="{{ $isAmount ? '1' : '0' }}"
@if($isTotalColumn) readonly @endif {{ $isReadOnly ? 'readonly' : '' }}
> >
</td> </td>
@endforeach @endforeach
@@ -706,186 +776,191 @@
</div> </div>
@if(!$container->rows->isEmpty()) @if(!$container->rows->isEmpty())
<button id="cmSaveBtnFloating" class="cm-save-btn-floating"> <button
Save Changes id="cmSaveBtnFloating"
</button> class="cm-save-btn-floating {{ $container->status !== 'pending' ? 'cm-disabled' : '' }}"
<div id="cmToast" class="cm-toast"> {{ $container->status !== 'pending' ? 'disabled' : '' }}
Changes saved successfully. >
</div> Save Changes
</button>
@endif @endif
<div id="cmToast" class="cm-toast"></div>
<script> <script>
function cmFilterRows() { function cmFilterRows() {
const input = document.getElementById('cmRowSearch'); const input = document.getElementById('cmRowSearch');
if (!input) return; if (!input) return;
const filter = input.value.toLowerCase(); const filter = input.value.toLowerCase();
const table = document.getElementById('cmExcelTable'); const table = document.getElementById('cmExcelTable');
if (!table) return; if (!table) return;
const rows = table.getElementsByTagName('tr'); const rows = table.getElementsByTagName('tr');
for (let i = 1; i < rows.length; i++) { for (let i = 1; i < rows.length; i++) {
const cells = rows[i].getElementsByTagName('td'); const cells = rows[i].getElementsByTagName('td');
let match = false; let match = false;
for (let j = 0; j < cells.length; j++) { for (let j = 0; j < cells.length; j++) {
const txt = cells[j].textContent || cells[j].innerText; const txt = cells[j].textContent || cells[j].innerText;
if (txt.toLowerCase().indexOf(filter) > -1) { if (txt.toLowerCase().indexOf(filter) > -1) {
match = true; match = true;
break; break;
}
} }
rows[i].style.display = match ? '' : 'none'; }
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;
} }
} }
document.addEventListener('DOMContentLoaded', function () { if (table) {
const form = document.getElementById('cm-edit-rows-form'); table.addEventListener('input', function (e) {
const btn = document.getElementById('cmSaveBtnFloating'); const target = e.target;
const toast = document.getElementById('cmToast'); if (!target.classList.contains('cm-cell-input')) return;
const table = document.getElementById('cmExcelTable');
function showToast(message, isError = false) { if (target.classList.contains('cm-cell-readonly') || target.hasAttribute('readonly')) {
if (!toast) return; target.blur();
toast.textContent = message; return;
toast.style.background = isError ? '#b91c1c' : '#0f172a'; }
toast.classList.add('cm-show');
setTimeout(() => toast.classList.remove('cm-show'), 2500);
}
function parseNumber(str) { const row = target.closest('tr');
if (!str) return 0; if (row) {
const cleaned = String(str).replace(/,/g, '').trim(); recalcRow(row);
const val = parseFloat(cleaned); }
return isNaN(val) ? 0 : val; });
} }
function formatNumber(val, decimals) { if (form && btn) {
if (isNaN(val)) val = 0; btn.addEventListener('click', function () {
return val.toFixed(decimals); if (btn.classList.contains('cm-disabled') || btn.hasAttribute('disabled')) {
} return;
}
function recalcRow(row) { btn.classList.add('cm-disabled');
const inputs = row.querySelectorAll('.cm-cell-input'); const formData = new FormData(form);
let ctn = 0, qty = 0, ttlQty = 0; fetch(form.action, {
let cbm = 0, ttlCbm = 0; method: 'POST',
let kg = 0, ttlKg = 0; headers: {
let price = 0, amount = 0; 'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': document.querySelector('input[name="_token"]').value
let ctnInput = null, },
qtyInput = null, body: formData
ttlQtyInput = null, })
cbmInput = null, .then(async res => {
ttlCbmInput = null, if (!res.ok) {
kgInput = null, const text = await res.text();
ttlKgInput = null, throw new Error(text || 'Failed to save');
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;
} }
return res.json().catch(() => ({}));
})
.then(() => {
showToast('Changes saved successfully.');
})
.catch(() => {
showToast('Error while saving changes.', true);
})
.finally(() => {
btn.classList.remove('cm-disabled');
}); });
});
if (ttlQtyInput && ctnInput && qtyInput) { }
const newTtlQty = ctn * qty; });
ttlQtyInput.value = formatNumber(newTtlQty, 0);
ttlQty = newTtlQty;
}
if (ttlCbmInput && cbmInput && ctnInput) {
const newTtlCbm = cbm * ctn;
ttlCbmInput.value = formatNumber(newTtlCbm, 3);
ttlCbm = newTtlCbm;
}
if (ttlKgInput && kgInput && ctnInput) {
const newTtlKg = kg * ctn;
ttlKgInput.value = formatNumber(newTtlKg, 2);
ttlKg = newTtlKg;
}
if (amountInput && priceInput && ttlQtyInput) {
const newAmount = price * ttlQty;
amountInput.value = formatNumber(newAmount, 2);
amount = newAmount;
}
}
if (table) {
table.addEventListener('input', function (e) {
const target = e.target;
if (!target.classList.contains('cm-cell-input')) return;
if (target.classList.contains('cm-cell-readonly') || target.hasAttribute('readonly')) {
target.blur();
return;
}
const row = target.closest('tr');
if (row) {
recalcRow(row);
}
});
}
if (form && btn) {
btn.addEventListener('click', function () {
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> </script>
@endsection @endsection

View File

@@ -661,7 +661,8 @@
<th class="table-header">Customer ID</th> <th class="table-header">Customer ID</th>
<th class="table-header">Orders</th> <th class="table-header">Orders</th>
<th class="table-header">Order Total</th> <th class="table-header">Order Total</th>
<th class="table-header">Total Payable</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">Remaining</th>
<th class="table-header">Create Date</th> <th class="table-header">Create Date</th>
<th class="table-header">Status</th> <th class="table-header">Status</th>
@@ -672,20 +673,27 @@
<tbody id="customersTableBody"> <tbody id="customersTableBody">
@forelse($customers as $c) @forelse($customers as $c)
@php @php
// Orders = invoice count // 1) Orders = total invoice count
$ordersCount = $c->invoices->count(); $ordersCount = $c->invoices->count();
// Order Total = items total from all invoices (final_amount) // 2) Order Total = सर्व invoices च्या charge groups चा base total (without GST)
$orderTotal = $c->invoices->sum('final_amount'); $orderTotal = $c->invoices->sum(function($invoice) {
return $invoice->chargeGroups->sum('total_charge');
});
// Total payable = grand total with GST + groups // 3) GST Amount = सर्व invoices च्या gst_amount चा sum
$totalPayable = $c->invoices->sum('grand_total_with_charges'); $gstTotal = $c->invoices->sum('gst_amount');
// Total paid via installments // 3) Total Payable = customer ने किती paid केले (installments sum)
$totalPaid = $c->invoiceInstallments->sum('amount'); $totalPaid = $c->invoices->flatMap->installments->sum('amount');
// Remaining amount // 4) Remaining = grand_total_with_charges - paid
$remainingAmount = max($totalPayable - $totalPaid, 0); $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 @endphp
<tr> <tr>
<td class="customer-info-column"> <td class="customer-info-column">
@@ -724,7 +732,13 @@
<td class="total-column"> <td class="total-column">
<span class="total-amount"> <span class="total-amount">
{{ number_format($totalPayable, 2) }} {{ number_format($gstTotal, 2) }}
</span>
</td>
<td class="total-column">
<span class="total-amount">
{{ number_format($totalPaid, 2) }}
</span> </span>
</td> </td>
@@ -771,7 +785,7 @@
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="9" 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> <i class="bi bi-people display-4 text-muted d-block mb-2"></i>
<span class="text-muted">No customers found.</span> <span class="text-muted">No customers found.</span>
</td> </td>

View File

@@ -1,30 +1,7 @@
@extends('admin.layouts.app') @extends('admin.layouts.app')
@section('page-title', 'Dashboard') @section('page-title', 'Dashboard')
@php
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Shipment;
use App\Models\Invoice;
use App\Models\User;
use App\Models\Admin;
$totalOrders = Order::count();
$pendingOrders = Order::where('status', 'pending')->count();
$totalShipments = Shipment::count();
$totalItems = OrderItem::count();
$totalRevenue = Invoice::sum('final_amount_with_gst');
// USERS (CUSTOMERS)
$activeCustomers = User::where('status', 'active')->count();
$inactiveCustomers = User::where('status', 'inactive')->count();
// STAFF (FROM ADMINS TABLE)
$totalStaff = Admin::where('type', 'staff')->count();
$orders = Order::latest()->get();
@endphp
@section('content') @section('content')
<style> <style>
/* ===== GLOBAL STYLES (From Shipment) ===== */ /* ===== GLOBAL STYLES (From Shipment) ===== */
@@ -1235,66 +1212,91 @@ body {
<!-- STATS CARDS --> <!-- STATS CARDS -->
<div class="stats-row-wrap"> <div class="stats-row-wrap">
{{-- Row 1: Total Containers, Active Customers, Total Invoices, Paid Invoices --}}
<div class="stats-row"> <div class="stats-row">
<div class="stats-card stats-card-blue"> <div class="stats-card stats-card-blue">
<span class="stats-icon">📦</span> <span class="stats-icon">&#x1F6A2;</span>
<div> <div>
<div class="stats-label">Total Shipments</div> <div class="stats-label">Total Containers</div>
<div class="stats-value">{{ $totalShipments }}</div> <div class="stats-value">{{ $totalContainers }}</div>
</div> </div>
</div> </div>
<div class="stats-card stats-card-blue"> <div class="stats-card stats-card-blue">
<span class="stats-icon">👥</span> <span class="stats-icon">&#x1F465;</span>
<div> <div>
<div class="stats-label">Active Customers</div> <div class="stats-label">Active Customers</div>
<div class="stats-value">{{ $activeCustomers }}</div> <div class="stats-value">{{ $activeCustomers }}</div>
</div> </div>
</div> </div>
<div class="stats-card stats-card-green"> <div class="stats-card stats-card-blue">
<span class="stats-icon">💰</span> <span class="stats-icon">&#x1F9FE;</span>
<div> <div>
<div class="stats-label">Total Revenue</div> <div class="stats-label">Total Orders</div>
<div class="stats-value">{{ number_format($totalRevenue, 2) }}</div> <div class="stats-value">{{ $totalInvoices }}</div>
</div> </div>
</div> </div>
<div class="stats-card stats-card-red"> <div class="stats-card stats-card-green">
<span class="stats-icon"></span> <span class="stats-icon">&#x2705;</span>
<div> <div>
<div class="stats-label">Pending Order</div> <div class="stats-label">Paid Invoices</div>
<div class="stats-value">{{ $pendingOrders }}</div> <div class="stats-value">{{ $paidInvoices }}</div>
</div> </div>
</div> </div>
</div> </div>
{{-- Row 2: Pending Invoices, Total Staff, Inactive Customers, Total Revenue --}}
<div class="stats-row"> <div class="stats-row">
<div class="stats-card stats-card-blue"> <div class="stats-card stats-card-orng">
<span class="stats-icon">📦</span> <span class="stats-icon">&#x1F550;</span>
<div> <div>
<div class="stats-label">Total Orders</div> <div class="stats-label">Pending Orders</div>
<div class="stats-value">{{ $totalOrders }}</div> <div class="stats-value">{{ $pendingInvoices }}</div>
</div> </div>
</div> </div>
<div class="stats-card stats-card-blue"> <div class="stats-card stats-card-blue">
<span class="stats-icon">🧑‍💼</span> <span class="stats-icon">&#x1F9D1;&#x200D;&#x1F4BC;</span>
<div> <div>
<div class="stats-label">Total Staff</div> <div class="stats-label">Total Staff</div>
<div class="stats-value">{{ $totalStaff }}</div> <div class="stats-value">{{ $totalStaff }}</div>
</div> </div>
</div> </div>
<div class="stats-card stats-card-blue">
<span class="stats-icon">📦</span>
<div>
<div class="stats-label">Total Items</div>
<div class="stats-value">{{ $totalItems }}</div>
</div>
</div>
<div class="stats-card stats-card-orng"> <div class="stats-card stats-card-orng">
<span class="stats-icon"></span> <span class="stats-icon">&#x26D4;</span>
<div> <div>
<div class="stats-label">Inactive Customers</div> <div class="stats-label">Inactive Customers</div>
<div class="stats-value">{{ $inactiveCustomers }}</div> <div class="stats-value">{{ $inactiveCustomers }}</div>
</div> </div>
</div> </div>
<div class="stats-card stats-card-green">
<span class="stats-icon">&#x1F4B0;</span>
<div>
<div class="stats-label">Total Revenue</div>
<div class="stats-value">&#8377;{{ number_format($totalRevenue, 2) }}</div>
</div>
</div>
</div> </div>
{{-- COMMENTED OUT --}}
{{--
<div class="stats-card stats-card-red">
<div class="stats-label">Pending Orders</div>
<div class="stats-value">{{ $pendingOrders }}</div>
</div>
<div class="stats-card stats-card-red">
<div class="stats-label">Overdue Invoices</div>
<div class="stats-value">{{ $overdueInvoices }}</div>
</div>
<div class="stats-card stats-card-blue">
<div class="stats-label">Total Orders</div>
<div class="stats-value">{{ $totalOrders }}</div>
</div>
<div class="stats-card stats-card-blue">
<div class="stats-label">Delivered Orders</div>
<div class="stats-value">{{ $totalOrders - $pendingOrders }}</div>
</div>
--}}
</div> </div>
<!-- ORDER MANAGEMENT --> <!-- ORDER MANAGEMENT -->

View File

@@ -170,6 +170,13 @@
font-weight: 500; font-weight: 500;
} }
.filter-select option[value="paying"] {
background-color: #e0e7ff;
color: #4338ca;
font-weight: 500;
}
.filter-select option[value="all"] { .filter-select option[value="all"] {
background-color: white; background-color: white;
color: #1f2937; color: #1f2937;
@@ -534,6 +541,10 @@
background: url('/images/status-bg-overdue.png') !important; background: url('/images/status-bg-overdue.png') !important;
} }
.badge-paying {
background: url('/images/status-bg-paying.png') !important;
}
/* Fallback colors if images don't load - ALL WITH SAME SIZE */ /* Fallback colors if images don't load - ALL WITH SAME SIZE */
.badge.badge-paid { .badge.badge-paid {
background: linear-gradient(135deg, #d1fae5, #a7f3d0) !important; background: linear-gradient(135deg, #d1fae5, #a7f3d0) !important;
@@ -554,6 +565,13 @@
border-color: #8b5cf6 !important; border-color: #8b5cf6 !important;
} }
.badge.badge-paying {
background: linear-gradient(135deg, #e0e7ff, #c7d2fe) !important;
color: #4338ca !important;
border-color: #6366f1 !important;
}
/* Entry Button - Centered */ /* Entry Button - Centered */
.btn-entry { .btn-entry {
background: linear-gradient(135deg, #3492f8 55%, #1256cc 110%); background: linear-gradient(135deg, #3492f8 55%, #1256cc 110%);
@@ -1218,7 +1236,7 @@
<th class="column-header">Customer</th> <th class="column-header">Customer</th>
<th class="column-header">Container</th> {{-- NEW --}} <th class="column-header">Container</th> {{-- NEW --}}
<th class="column-header">Final Amount</th> <th class="column-header">Final Amount</th>
<th class="column-header">GST %</th> <th class="column-header">GST Amount</th>
<th class="column-header">Total w/GST</th> <th class="column-header">Total w/GST</th>
<th class="column-header">Status</th> <th class="column-header">Status</th>
<th class="column-header">Invoice Date</th> <th class="column-header">Invoice Date</th>
@@ -1268,8 +1286,8 @@
{{ number_format($invoice->final_amount, 2) }} {{ number_format($invoice->final_amount, 2) }}
</td> </td>
<td class="gst-cell"> <td class="amount-cell">
{{ $invoice->gst_percent }}% {{ number_format($invoice->gst_amount, 2) }}
</td> </td>
<td class="amount-cell"> <td class="amount-cell">
@@ -1277,16 +1295,19 @@
</td> </td>
<td> <td>
<span class="badge badge-{{ $invoice->status }}"> <span class="badge badge-{{ $invoice->status }}">
@if($invoice->status == 'paid') @if($invoice->status == 'paid')
<i class="bi bi-check-circle-fill status-icon"></i> <i class="bi bi-check-circle-fill status-icon"></i>
@elseif($invoice->status == 'pending') @elseif($invoice->status == 'pending')
<i class="bi bi-clock-fill status-icon"></i> <i class="bi bi-clock-fill status-icon"></i>
@elseif($invoice->status == 'overdue') @elseif($invoice->status == 'paying')
<i class="bi bi-exclamation-triangle-fill status-icon"></i> <i class="bi bi-arrow-repeat status-icon"></i> {{-- processing icon --}}
@endif @elseif($invoice->status == 'overdue')
{{ ucfirst($invoice->status) }} <i class="bi bi-exclamation-triangle-fill status-icon"></i>
</span> @endif
{{ ucfirst($invoice->status) }}
</span>
</td> </td>
<td class="date-cell"> <td class="date-cell">
@@ -1338,11 +1359,14 @@
<i class="bi bi-check-circle-fill status-icon"></i> <i class="bi bi-check-circle-fill status-icon"></i>
@elseif($invoice->status == 'pending') @elseif($invoice->status == 'pending')
<i class="bi bi-clock-fill status-icon"></i> <i class="bi bi-clock-fill status-icon"></i>
@elseif($invoice->status == 'paying')
<i class="bi bi-arrow-repeat status-icon"></i>
@elseif($invoice->status == 'overdue') @elseif($invoice->status == 'overdue')
<i class="bi bi-exclamation-triangle-fill status-icon"></i> <i class="bi bi-exclamation-triangle-fill status-icon"></i>
@endif @endif
{{ ucfirst($invoice->status) }} {{ ucfirst($invoice->status) }}
</span> </span>
</div> </div>
<div class="mobile-invoice-details"> <div class="mobile-invoice-details">
@@ -1355,8 +1379,8 @@
<span class="mobile-detail-value">{{ number_format($invoice->final_amount, 2) }}</span> <span class="mobile-detail-value">{{ number_format($invoice->final_amount, 2) }}</span>
</div> </div>
<div class="mobile-detail-item"> <div class="mobile-detail-item">
<span class="mobile-detail-label">GST</span> <span class="mobile-detail-label">GST Amount</span>
<span class="mobile-detail-value">{{ $invoice->gst_percent }}%</span> <span class="mobile-detail-value">{{ number_format($invoice->gst_amount, 2) }}</span>
</div> </div>
<div class="mobile-detail-item"> <div class="mobile-detail-item">
<span class="mobile-detail-label">Total</span> <span class="mobile-detail-label">Total</span>
@@ -1670,13 +1694,14 @@ document.addEventListener('DOMContentLoaded', function() {
${invoice.container ? (invoice.container.container_number ?? '—') : '—'} ${invoice.container ? (invoice.container.container_number ?? '—') : '—'}
</td> </td>
<td class="amount-cell">${parseFloat(invoice.final_amount).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</td> <td class="amount-cell">${parseFloat(invoice.final_amount).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</td>
<td class="gst-cell">${invoice.gst_percent}%</td> <td class="amount-cell">${parseFloat(invoice.gst_amount).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</td>
<td class="amount-cell">${parseFloat(invoice.final_amount_with_gst).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</td> <td class="amount-cell">${parseFloat(invoice.final_amount_with_gst).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</td>
<td> <td>
<span class="badge badge-${invoice.status}"> <span class="badge badge-${invoice.status}">
${invoice.status === 'paid' ? '<i class="bi bi-check-circle-fill status-icon"></i>' : ''} ${invoice.status === 'paid' ? '<i class="bi bi-check-circle-fill status-icon"></i>' : ''}
${invoice.status === 'pending' ? '<i class="bi bi-clock-fill status-icon"></i>' : ''} ${invoice.status === 'pending' ? '<i class="bi bi-clock-fill status-icon"></i>' : ''}
${invoice.status === 'overdue' ? '<i class="bi bi-exclamation-triangle-fill status-icon"></i>' : ''} ${invoice.status === 'overdue' ? '<i class="bi bi-exclamation-triangle-fill status-icon"></i>' : ''}
${invoice.status === 'paying' ? '<i class="bi bi-arrow-repeat status-icon"></i>' : ''}
${invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)} ${invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
</span> </span>
</td> </td>
@@ -1710,6 +1735,7 @@ document.addEventListener('DOMContentLoaded', function() {
${invoice.status === 'paid' ? '<i class="bi bi-check-circle-fill status-icon"></i>' : ''} ${invoice.status === 'paid' ? '<i class="bi bi-check-circle-fill status-icon"></i>' : ''}
${invoice.status === 'pending' ? '<i class="bi bi-clock-fill status-icon"></i>' : ''} ${invoice.status === 'pending' ? '<i class="bi bi-clock-fill status-icon"></i>' : ''}
${invoice.status === 'overdue' ? '<i class="bi bi-exclamation-triangle-fill status-icon"></i>' : ''} ${invoice.status === 'overdue' ? '<i class="bi bi-exclamation-triangle-fill status-icon"></i>' : ''}
${invoice.status === 'paying' ? '<i class="bi bi-arrow-repeat status-icon"></i>' : ''}
${invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)} ${invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
</span> </span>
</div> </div>
@@ -1730,8 +1756,8 @@ document.addEventListener('DOMContentLoaded', function() {
<span class="mobile-detail-value">${parseFloat(invoice.final_amount).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</span> <span class="mobile-detail-value">${parseFloat(invoice.final_amount).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</span>
</div> </div>
<div class="mobile-detail-item"> <div class="mobile-detail-item">
<span class="mobile-detail-label">GST</span> <span class="mobile-detail-label">GST Amount</span>
<span class="mobile-detail-value">${invoice.gst_percent}%</span> <span class="mobile-detail-value">${parseFloat(invoice.gst_amount).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</span>
</div> </div>
<div class="mobile-detail-item"> <div class="mobile-detail-item">
<span class="mobile-detail-label">Total</span> <span class="mobile-detail-label">Total</span>

View File

@@ -1,9 +1,7 @@
@extends('admin.layouts.app') @extends('admin.layouts.app')
@section('page-title', 'Edit Invoice') @section('page-title', 'Edit Invoice')
@section('content') @section('content')
<style> <style>
:root { :root {
@@ -314,7 +312,7 @@
</style> </style>
<div class="container-fluid py-3"> <div class="container-fluid py-3">
{{-- Invoice Preview / Overview --}} {{-- Invoice Overview --}}
<div class="glass-card"> <div class="glass-card">
<div class="card-header-compact"> <div class="card-header-compact">
<h4> <h4>
@@ -323,237 +321,66 @@
</h4> </h4>
</div> </div>
<div class="card-body-compact"> <div class="card-body-compact">
{{-- Read-only popup: items price/total cannot be edited here --}}
@include('admin.popup_invoice', ['invoice' => $invoice, 'shipment' => $shipment]) @include('admin.popup_invoice', ['invoice' => $invoice, 'shipment' => $shipment])
</div> </div>
</div> </div>
{{-- Edit Invoice Header Details (POST) --}} @if(false)
<div class="glass-card"> {{-- Edit Invoice Details card HIDDEN --}}
<div class="card-header-compact"> {{-- तुझा full header edit form इथे hidden ठेवलेला आहे --}}
<h4> @endif
<i class="fas fa-edit me-2"></i>
Edit Invoice Details
</h4>
</div>
<div class="card-body-compact">
<form action="{{ route('admin.invoices.update', $invoice->id) }}" method="POST">
@csrf
<div class="form-grid-compact">
{{-- Invoice Date --}}
<div class="form-group-compact">
<label class="form-label-compact">
<i class="fas fa-calendar-day"></i> Invoice Date
</label>
<input type="date"
name="invoice_date"
class="form-control-compact"
value="{{ old('invoice_date', $invoice->invoice_date) }}"
required>
</div>
{{-- Due Date --}}
<div class="form-group-compact">
<label class="form-label-compact">
<i class="fas fa-clock"></i> Due Date
</label>
<input type="date"
name="due_date"
class="form-control-compact"
value="{{ old('due_date', $invoice->due_date) }}"
required>
</div>
{{-- Final Amount (Base) --}}
<div class="form-group-compact">
<label class="form-label-compact">
<i class="fas fa-money-bill-wave"></i> Final Amount (Before GST)
</label>
<input type="number"
step="0.01"
name="final_amount"
class="form-control-compact"
value="{{ old('final_amount', $invoice->final_amount) }}"
required>
</div>
{{-- Tax Type --}}
<div class="form-group-compact">
<label class="form-label-compact">
<i class="fas fa-receipt"></i> Tax Type
</label>
<div class="d-flex gap-3 mt-1">
<div class="form-check">
<input class="form-check-input"
type="radio"
name="tax_type"
value="gst"
{{ old('tax_type', $invoice->tax_type) === 'gst' ? 'checked' : '' }}>
<label class="form-check-label fw-semibold">GST (CGST + SGST)</label>
</div>
<div class="form-check">
<input class="form-check-input"
type="radio"
name="tax_type"
value="igst"
{{ old('tax_type', $invoice->tax_type) === 'igst' ? 'checked' : '' }}>
<label class="form-check-label fw-semibold">IGST</label>
</div>
</div>
</div>
{{-- Tax Percentage --}}
<div class="form-group-compact">
<label class="form-label-compact">
<i class="fas fa-percentage"></i> Tax Percentage
</label>
<input type="number"
step="0.01"
min="0"
max="28"
name="tax_percent"
class="form-control-compact"
value="{{ old('tax_percent',
$invoice->tax_type === 'gst'
? ($invoice->cgst_percent + $invoice->sgst_percent)
: $invoice->igst_percent
) }}"
required>
</div>
{{-- Status --}}
<div class="form-group-compact">
<label class="form-label-compact">
<i class="fas fa-tasks"></i> Status
</label>
<select name="status" class="form-select-compact" required>
<option value="pending" {{ old('status', $invoice->status) === 'pending' ? 'selected' : '' }}>
Pending
</option>
<option value="paid" {{ old('status', $invoice->status) === 'paid' ? 'selected' : '' }}>
Paid
</option>
<option value="overdue" {{ old('status', $invoice->status) === 'overdue' ? 'selected' : '' }}>
Overdue
</option>
</select>
</div>
{{-- Notes --}}
<div class="form-group-compact" style="grid-column: 1 / -1;">
<label class="form-label-compact">
<i class="fas fa-sticky-note"></i> Notes
</label>
<textarea name="notes"
rows="3"
class="form-control-compact"
placeholder="Add any additional notes...">{{ old('notes', $invoice->notes) }}</textarea>
</div>
</div>
<div class="text-end mt-3">
<button type="submit" class="btn-success-compact btn-compact">
<i class="fas fa-save me-2"></i>Update Invoice
</button>
</div>
</form>
</div>
</div>
@php @php
// आता helpers वापरू: totalPaid() आणि remainingAmount() $totalPaid = $invoice->totalPaid();
$totalPaid = $invoice->totalPaid(); $remaining = $invoice->remainingAmount();
$remaining = $invoice->remainingAmount();
$taxTypes = $invoice->chargeGroups
? $invoice->chargeGroups->pluck('tax_type')->filter()->unique()->values()
: collect([]);
$taxTypeLabel = 'None';
if ($taxTypes->count() === 1) {
if ($taxTypes[0] === 'gst') {
$taxTypeLabel = 'GST (CGST + SGST)';
} elseif ($taxTypes[0] === 'igst') {
$taxTypeLabel = 'IGST';
} else {
$taxTypeLabel = strtoupper($taxTypes[0]);
}
} elseif ($taxTypes->count() > 1) {
$parts = [];
if ($taxTypes->contains('gst')) {
$parts[] = 'GST (CGST + SGST)';
}
if ($taxTypes->contains('igst')) {
$parts[] = 'IGST';
}
$taxTypeLabel = implode(' + ', $parts);
}
@endphp @endphp
{{-- Amount Breakdown (items + GST + groups) --}} @if(false)
<div class="amount-breakdown-compact"> {{-- Amount Breakdown HIDDEN --}}
<h6 class="fw-bold mb-3 text-dark"> {{-- जुनं breakdown section इथे hidden आहे --}}
<i class="fas fa-calculator me-2"></i>Amount Breakdown @endif
</h6>
<div class="breakdown-row"> {{-- Summary cards --}}
<span class="breakdown-label">Total Amount Before Tax</span>
<span class="breakdown-value" id="baseAmount">
{{ number_format($invoice->final_amount, 2) }}
</span>
</div>
<div class="breakdown-row">
<span class="breakdown-label">Tax Type</span>
<span class="breakdown-value text-primary">
@if($invoice->tax_type === 'gst')
GST (CGST + SGST)
@else
IGST
@endif
</span>
</div>
<div class="breakdown-row">
<span class="breakdown-label">Tax Percentage</span>
<span class="breakdown-value text-primary">
@if($invoice->tax_type === 'gst')
{{ $invoice->cgst_percent + $invoice->sgst_percent }}%
@else
{{ $invoice->igst_percent }}%
@endif
</span>
</div>
<div class="breakdown-row">
<span class="breakdown-label">GST Amount</span>
<span class="breakdown-value text-warning" id="gstAmount">
{{ number_format($invoice->gst_amount, 2) }}
</span>
</div>
<div class="breakdown-row">
<span class="breakdown-label">Charge Groups Total</span>
<span class="breakdown-value text-info" id="chargeGroupsTotal">
{{ number_format($invoice->charge_groups_total, 2) }}
</span>
</div>
<div class="breakdown-row" style="border-top: 2px solid #e2e8f0; padding-top: 0.75rem;">
<span class="breakdown-label fw-bold">Grand Total (Items + GST + Groups)</span>
<span class="breakdown-value fw-bold text-dark" id="totalInvoiceWithGst">
{{ number_format($invoice->grand_total_with_charges, 2) }}
</span>
</div>
<div class="breakdown-row">
<span class="breakdown-label text-success">Total Paid</span>
<span class="breakdown-value fw-bold text-success" id="paidAmount">
{{ number_format($totalPaid, 2) }}
</span>
</div>
<div class="breakdown-row" style="border-bottom: none;">
<span class="breakdown-label text-danger">Remaining</span>
<span class="breakdown-value fw-bold text-danger" id="remainingAmount">
{{ number_format(max(0, $remaining), 2) }}
</span>
</div>
</div>
{{-- Installment Summary (top cards) --}}
<div class="summary-grid-compact"> <div class="summary-grid-compact">
<div class="summary-card-compact total"> <div class="summary-card-compact total">
<div class="summary-value-compact text-success" id="totalInvoiceWithGstCard"> <div class="summary-value-compact text-success" id="totalInvoiceWithGstCard">
{{ number_format($invoice->grand_total_with_charges, 2) }} {{ number_format($invoice->grand_total_with_charges, 2) }}
</div> </div>
<div class="summary-label-compact">Grand Total (Items + GST + Groups)</div> <div class="summary-label-compact">Grand Total (Charges + GST)</div>
</div> </div>
<div class="summary-card-compact paid"> <div class="summary-card-compact paid">
<div class="summary-value-compact text-primary"> <div class="summary-value-compact text-primary" id="paidAmount">
{{ number_format($totalPaid, 2) }} {{ number_format($totalPaid, 2) }}
</div> </div>
<div class="summary-label-compact">Total Paid</div> <div class="summary-label-compact">Total Paid</div>
</div> </div>
<div class="summary-card-compact remaining"> <div class="summary-card-compact remaining">
<div class="summary-value-compact text-warning"> <div class="summary-value-compact text-warning" id="remainingAmount">
{{ number_format(max(0, $remaining), 2) }} {{ number_format(max(0, $remaining), 2) }}
</div> </div>
<div class="summary-label-compact">Remaining</div> <div class="summary-label-compact">Remaining</div>
@@ -739,87 +566,128 @@ document.addEventListener("DOMContentLoaded", function () {
headers: { headers: {
"X-CSRF-TOKEN": submitForm.querySelector('input[name="_token"]').value, "X-CSRF-TOKEN": submitForm.querySelector('input[name="_token"]').value,
"Accept": "application/json", "Accept": "application/json",
"X-Requested-With": "XMLHttpRequest",
}, },
body: new FormData(submitForm) body: new FormData(submitForm)
}) })
.then(res => res.json()) .then(async res => {
.then(data => { let data;
submitBtn.innerHTML = '<i class="fas fa-paper-plane me-2"></i>Submit Installment'; try {
submitBtn.disabled = false; data = await res.json();
} catch (e) {
throw new Error("Invalid server response.");
}
if (data.status === "error") { submitBtn.innerHTML = '<i class="fas fa-paper-plane me-2"></i>Submit Installment';
alert(data.message); submitBtn.disabled = false;
return;
}
const table = document.getElementById("installmentTable"); if (!res.ok) {
const index = table.rows.length + 1; const msg =
(data && data.message) ||
(data && data.errors && Object.values(data.errors)[0][0]) ||
"Something went wrong.";
alert(msg);
return;
}
document.getElementById("noInstallmentsMsg")?.classList.add("d-none"); if (data.status === "error") {
alert(data.message || "Something went wrong.");
return;
}
table.insertAdjacentHTML("beforeend", ` const table = document.getElementById("installmentTable");
<tr data-id="${data.installment.id}"> const index = table.querySelectorAll("tr").length + 1;
<td class="fw-bold text-muted">${index}</td>
<td>${data.installment.installment_date}</td>
<td>
<span class="badge-compact bg-primary bg-opacity-10 text-primary">
${data.installment.payment_method.toUpperCase()}
</span>
</td>
<td>
${data.installment.reference_no
? `<span class="text-muted">${data.installment.reference_no}</span>`
: '<span class="text-muted">-</span>'}
</td>
<td class="fw-bold text-success">
${formatINR(data.installment.amount)}
</td>
<td>
<button class="btn-danger-compact btn-compact btn-sm deleteInstallment">
<i class="fas fa-trash me-1"></i>Delete
</button>
</td>
</tr>
`);
if (document.getElementById("paidAmount")) { document.getElementById("noInstallmentsMsg")?.classList.add("d-none");
document.getElementById("paidAmount").textContent = "" + formatINR(data.totalPaid);
}
if (document.getElementById("remainingAmount")) {
document.getElementById("remainingAmount").textContent = "" + formatINR(data.remaining);
}
if (document.getElementById("baseAmount")) {
document.getElementById("baseAmount").textContent = "" + formatINR(data.baseAmount);
}
if (document.getElementById("gstAmount")) {
document.getElementById("gstAmount").textContent = "" + formatINR(data.gstAmount);
}
// grand total आता finalAmountWithGst नाही, पण API अजून तेच key देत आहे,
// त्यामुळे इथे फक्त card आणि breakdown value update करतो:
if (document.getElementById("totalInvoiceWithGst")) {
document.getElementById("totalInvoiceWithGst").textContent = "" + formatINR(data.finalAmountWithGst);
}
const paidCard = document.querySelector(".summary-card-compact.paid .summary-value-compact"); const pmMethod = data.installment.payment_method
if (paidCard) paidCard.textContent = "" + formatINR(data.totalPaid); ? data.installment.payment_method.toUpperCase()
: "-";
const refNo = data.installment.reference_no
? `<span class="text-muted">${data.installment.reference_no}</span>`
: '<span class="text-muted">-</span>';
const remainingCard = document.querySelector(".summary-card-compact.remaining .summary-value-compact"); table.insertAdjacentHTML("beforeend", `
if (remainingCard) remainingCard.textContent = "" + formatINR(data.remaining); <tr data-id="${data.installment.id}">
<td class="fw-bold text-muted">${index}</td>
<td>${data.installment.installment_date}</td>
<td>
<span class="badge-compact bg-primary bg-opacity-10 text-primary">
${pmMethod}
</span>
</td>
<td>${refNo}</td>
<td class="fw-bold text-success">
${formatINR(data.installment.amount)}
</td>
<td>
<button class="btn-danger-compact btn-compact btn-sm deleteInstallment">
<i class="fas fa-trash me-1"></i>Delete
</button>
</td>
</tr>
`);
submitForm.reset(); if (document.getElementById("paidAmount")) {
document.getElementById("paidAmount").textContent = "" + formatINR(data.totalPaid);
}
if (document.getElementById("remainingAmount")) {
document.getElementById("remainingAmount").textContent = "" + formatINR(data.remaining);
}
if (document.getElementById("baseAmount")) {
document.getElementById("baseAmount").textContent = "" + formatINR(data.chargeGroupsTotal);
}
if (document.getElementById("gstAmount")) {
document.getElementById("gstAmount").textContent = "" + formatINR(data.gstAmount);
}
if (document.getElementById("totalInvoiceWithGst")) {
document.getElementById("totalInvoiceWithGst").textContent =
"" + formatINR(data.grandTotal);
}
const totalCard = document.getElementById("totalInvoiceWithGstCard");
if (totalCard) {
totalCard.textContent = "" + formatINR(data.grandTotal);
}
if (data.isCompleted && toggleBtn) { const paidCard = document.querySelector(".summary-card-compact.paid .summary-value-compact");
toggleBtn.remove(); if (paidCard) paidCard.textContent = "" + formatINR(data.totalPaid);
formBox.classList.add("d-none");
}
alert(data.message); const remainingCard = document.querySelector(".summary-card-compact.remaining .summary-value-compact");
}) if (remainingCard) remainingCard.textContent = "" + formatINR(data.remaining);
.catch(() => {
submitBtn.innerHTML = '<i class="fas fa-paper-plane me-2"></i>Submit Installment'; const fsTotal = document.getElementById("finalSummaryTotalPaid");
submitBtn.disabled = false; if (fsTotal) fsTotal.textContent = "" + formatINR(data.totalPaid);
alert("Something went wrong. Please try again.");
}); const fsRemaining = document.getElementById("finalSummaryRemaining");
if (fsRemaining) {
fsRemaining.textContent = "" + formatINR(data.remaining);
fsRemaining.style.color = data.remaining > 0 ? "#ef4444" : "#10b981";
}
const fsRemainingLabel = document.getElementById("finalSummaryRemainingLabel");
if (fsRemainingLabel) {
fsRemainingLabel.style.color = data.remaining > 0 ? "#ef4444" : "#10b981";
fsRemainingLabel.innerHTML = `<i class="fas fa-${data.remaining > 0 ? 'exclamation-circle' : 'check-circle'} me-1" style="font-size:0.8rem;"></i>Remaining`;
}
if (data.newStatus && typeof updateStatusBadge === "function") {
updateStatusBadge(data.newStatus);
}
submitForm.reset();
if (data.isCompleted && toggleBtn) {
toggleBtn.remove();
formBox.classList.add("d-none");
}
alert(data.message || "Installment added successfully.");
})
.catch((err) => {
console.error("Installment submit error:", err);
submitBtn.innerHTML = '<i class="fas fa-paper-plane me-2"></i>Submit Installment';
submitBtn.disabled = false;
alert(err.message || "Something went wrong. Please try again.");
});
}); });
} }
@@ -843,66 +711,138 @@ document.addEventListener("DOMContentLoaded", function () {
headers: { headers: {
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').getAttribute("content"), "X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').getAttribute("content"),
"Accept": "application/json", "Accept": "application/json",
"X-Requested-With": "XMLHttpRequest",
}, },
}) })
.then(res => res.json()) .then(async res => {
.then(data => { let data;
if (data.status === "success") { try { data = await res.json(); } catch(e) { throw new Error("Invalid server response."); }
row.style.opacity = 0;
setTimeout(() => row.remove(), 300);
if (document.getElementById("paidAmount")) { if (!res.ok || data.status !== "success") {
document.getElementById("paidAmount").textContent = "" + formatINR(data.totalPaid); const msg = data && data.message ? data.message : "Something went wrong. Please try again.";
} throw new Error(msg);
if (document.getElementById("remainingAmount")) { }
document.getElementById("remainingAmount").textContent = "" + formatINR(data.remaining);
}
if (document.getElementById("baseAmount")) {
document.getElementById("baseAmount").textContent = "" + formatINR(data.baseAmount);
}
if (document.getElementById("gstAmount")) {
document.getElementById("gstAmount").textContent = "" + formatINR(data.gstAmount);
}
if (document.getElementById("totalInvoiceWithGst")) {
document.getElementById("totalInvoiceWithGst").textContent = "" + formatINR(data.finalAmountWithGst);
}
const paidCard = document.querySelector(".summary-card-compact.paid .summary-value-compact"); row.style.opacity = 0;
if (paidCard) paidCard.textContent = "" + formatINR(data.totalPaid); setTimeout(() => row.remove(), 300);
const remainingCard = document.querySelector(".summary-card-compact.remaining .summary-value-compact"); if (document.getElementById("paidAmount")) {
if (remainingCard) remainingCard.textContent = "" + formatINR(data.remaining); document.getElementById("paidAmount").textContent = "" + formatINR(data.totalPaid);
}
if (document.getElementById("remainingAmount")) {
document.getElementById("remainingAmount").textContent = "" + formatINR(data.remaining);
}
if (document.getElementById("baseAmount")) {
document.getElementById("baseAmount").textContent = "" + formatINR(data.chargeGroupsTotal);
}
if (document.getElementById("gstAmount")) {
document.getElementById("gstAmount").textContent = "" + formatINR(data.gstAmount);
}
if (document.getElementById("totalInvoiceWithGst")) {
document.getElementById("totalInvoiceWithGst").textContent =
"" + formatINR(data.grandTotal);
}
const totalCard = document.getElementById("totalInvoiceWithGstCard");
if (totalCard) {
totalCard.textContent = "" + formatINR(data.grandTotal);
}
if (data.isZero) { const paidCard = document.querySelector(".summary-card-compact.paid .summary-value-compact");
document.getElementById("noInstallmentsMsg")?.classList.remove("d-none"); if (paidCard) paidCard.textContent = "" + formatINR(data.totalPaid);
const remainingCard = document.querySelector(".summary-card-compact.remaining .summary-value-compact");
if (remainingCard) remainingCard.textContent = "" + formatINR(data.remaining);
const fsTotal = document.getElementById("finalSummaryTotalPaid");
if (fsTotal) fsTotal.textContent = "" + formatINR(data.totalPaid);
const fsRemaining = document.getElementById("finalSummaryRemaining");
if (fsRemaining) {
fsRemaining.textContent = "" + formatINR(data.remaining);
fsRemaining.style.color = data.remaining > 0 ? "#ef4444" : "#10b981";
}
const fsRemainingLabel = document.getElementById("finalSummaryRemainingLabel");
if (fsRemainingLabel) {
fsRemainingLabel.style.color = data.remaining > 0 ? "#ef4444" : "#10b981";
fsRemainingLabel.innerHTML = `<i class="fas fa-${data.remaining > 0 ? 'exclamation-circle' : 'check-circle'} me-1" style="font-size:0.8rem;"></i>Remaining`;
}
if (data.newStatus && typeof updateStatusBadge === "function") {
updateStatusBadge(data.newStatus);
}
if (data.isZero) {
document.getElementById("noInstallmentsMsg")?.classList.remove("d-none");
}
alert(data.message || "Installment deleted.");
})
.catch(err => {
alert(err.message || "Something went wrong. Please try again.");
});
});
// Header AJAX (सध्या card hidden आहे, तरीही ठेवलेलं)
const headerForm = document.getElementById('invoiceHeaderForm');
const headerBtn = document.getElementById('btnHeaderSave');
const headerMsg = document.getElementById('headerUpdateMsg');
if (headerForm && headerBtn) {
headerForm.addEventListener('submit', function(e) {
e.preventDefault();
headerMsg.textContent = '';
headerBtn.disabled = true;
headerBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
const formData = new FormData(headerForm);
fetch(headerForm.action, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
body: formData
})
.then(async res => {
let data;
try { data = await res.json(); } catch(e) {}
if (!res.ok) {
if (data && data.errors) {
const firstError = Object.values(data.errors)[0][0] ?? 'Validation error.';
throw new Error(firstError);
} }
throw new Error(data && data.message ? data.message : 'Failed to update invoice.');
}
alert(data.message); headerMsg.textContent = 'Invoice header updated.';
} else { headerMsg.classList.remove('text-danger');
alert(data.message || "Something went wrong. Please try again."); headerMsg.classList.add('text-light');
const status = document.getElementById('statusSelect')?.value;
const badge = document.querySelector('.status-badge');
if (badge && status) {
badge.classList.remove('status-paid','status-pending','status-overdue','status-default');
if (status === 'paid') badge.classList.add('status-paid');
else if (status === 'pending') badge.classList.add('status-pending');
else if (status === 'overdue') badge.classList.add('status-overdue');
else badge.classList.add('status-default');
badge.innerHTML = status.charAt(0).toUpperCase() + status.slice(1);
} }
}) })
.catch(() => { .catch(err => {
alert("Something went wrong. Please try again."); headerMsg.textContent = err.message || 'Error updating invoice.';
headerMsg.classList.remove('text-light');
headerMsg.classList.add('text-warning');
})
.finally(() => {
headerBtn.disabled = false;
headerBtn.innerHTML = '<i class="fas fa-save me-2"></i>Update Invoice';
}); });
});
});
document.addEventListener('DOMContentLoaded', function() {
const invoiceDateInput = document.querySelector('input[name="invoice_date"]');
const dueDateInput = document.querySelector('input[name="due_date"]');
if (invoiceDateInput && dueDateInput) {
invoiceDateInput.addEventListener('change', function() {
const selectedDate = new Date(this.value);
if (!isNaN(selectedDate.getTime())) {
selectedDate.setDate(selectedDate.getDate() + 10);
const year = selectedDate.getFullYear();
const month = String(selectedDate.getMonth() + 1).padStart(2, '0');
const day = String(selectedDate.getDate()).padStart(2, '0');
dueDateInput.value = `${year}-${month}-${day}`;
}
}); });
} }
}); });

View File

@@ -546,12 +546,6 @@
</div> </div>
<div class="stats-container"> <div class="stats-container">
@php
$totalInvoices = $invoices->count();
$paidInvoices = $invoices->where('invoice_status', 'paid')->count();
$pendingInvoices = $invoices->where('invoice_status', 'pending')->count();
$overdueInvoices = $invoices->where('invoice_status', 'overdue')->count();
@endphp
<div class="stat-card total"> <div class="stat-card total">
<div class="stat-icon-wrap"> <div class="stat-icon-wrap">

View File

@@ -1,272 +1,623 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{ $invoice->invoice_number }}</title> <title>Invoice #{{ $invoice->invoice_number }}</title>
<style> <style>
body { body {
font-family: 'Segoe UI', 'Roboto', Arial, sans-serif; font-family: 'DejaVu Sans', sans-serif;
background: #F7FBFC; font-size: 10px;
color: #1A222B; color: #1a202c;
line-height: 1.4;
margin: 0; margin: 0;
padding: 0; padding: 18px;
font-size: 15px;
}
.container {
max-width: 850px;
margin: 24px auto 0 auto;
background: #fff; background: #fff;
border-radius: 13px;
box-shadow: 0 2px 14px rgba(40,105,160,0.08);
padding: 35px 32px 18px 32px;
} }
.header {
display: flex; /* ── TOP HEADER ── */
justify-content: space-between; .top-header {
align-items: flex-start;
border-bottom: 2px solid #E6EBF0;
padding-bottom: 13px;
}
.logo-company {
display: flex;
align-items: flex-start;
}
.logo {
height: 50px;
margin-right: 13px;
}
.company-details {
margin-top: 0;
font-size: 15px;
}
.company-title {
font-size: 21px;
font-weight: bold;
margin-bottom: 2px;
}
.company-sub {
font-size: 16px;
margin-bottom: 8px; margin-bottom: 8px;
font-weight: 500; border-bottom: 2px solid #1a202c;
padding-bottom: 6px;
} }
.invoice-details { .header-table {
text-align: right;
min-width: 220px;
}
.invoice-title {
font-weight: bold;
font-size: 23px;
letter-spacing: 0.5px;
margin-bottom: 2px;
}
.paid-label {
margin-top: 8px;
margin-bottom: 2px;
}
.paid-tag {
background: #23BF47;
color: #fff;
font-weight: bold;
border-radius: 8px;
padding: 4px 16px 4px 22px;
font-size: 17px;
display: inline-block;
position: relative;
}
.paid-tag:before {
content: '';
position: absolute;
left: 7px;
top: 7px;
width: 10px;
height: 10px;
background: #fff;
border-radius: 50%;
}
.paid-date {
font-size: 14px;
color: #23BF47;
margin-top: 2px;
}
.bill-section {
background: #F3F7FB;
border-radius: 11px;
padding: 20px 18px 13px 18px;
margin: 28px 0 16px 0;
box-shadow: 0 0px 0px #0000;
}
.bill-title {
font-size: 17px;
font-weight: bold;
color: #23355D;
margin-bottom: 4px;
letter-spacing: 0.3px;
}
.bill-details {
font-size: 15px;
line-height: 1.6;
}
table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin-top: 9px;
margin-bottom: 13px;
background: #fff;
border-radius: 10px;
overflow: hidden;
} }
th { .header-table td {
background: #F6F7F9; vertical-align: middle;
padding: 10px 0; padding: 0;
font-size: 15px; }
color: #6781A6; /* Logo cell — fixed width, logo fits inside */
font-weight: bold; .logo-cell {
border: none; width: 60px;
}
.logo-cell img {
width: 55px;
height: 55px;
display: block;
}
/* Company name cell */
.title-cell {
padding-left: 10px;
text-align: left; text-align: left;
} }
td { .company-name {
padding: 7px 0; font-size: 18px;
color: #222;
font-size: 15px;
border: none;
text-align: left;
}
tbody tr:not(:last-child) td {
border-bottom: 1px solid #E6EBF0;
}
tbody tr:last-child td {
border-bottom: none;
}
.totals-row td {
font-weight: bold; font-weight: bold;
color: #23355D; color: #1a202c;
text-transform: uppercase;
letter-spacing: 2px;
} }
.gst-row td { .company-subtitle {
font-weight: 500; font-size: 11px;
color: #23BF47;
}
.total-row td {
font-weight: bold; font-weight: bold;
font-size: 17px; color: #2d3748;
color: #222; letter-spacing: 1px;
} }
.payment-info { .company-tagline {
margin-top: 24px; font-size: 8px;
margin-bottom: 9px; color: #718096;
font-size: 15px; margin-top: 3px;
} }
.ref-number {
font-size: 14px; /* ── INFO TABLE (Customer | Invoice side by side) ── */
color: #6781A6; .info-outer {
margin-bottom: 8px; width: 100%;
margin-top: 2px; border-collapse: collapse;
border: 1px solid #cbd5e0;
margin-bottom: 10px;
} }
.footer { .info-outer td {
border-top: 1.2px solid #E6EBF0; vertical-align: top;
margin-top: 25px; padding: 10px 12px;
padding-top: 12px; }
font-size: 16px; .info-left { width: 50%; }
color: #888; .info-right { width: 50%; border-left: 1px solid #cbd5e0; }
.section-title {
font-size: 8px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1px;
color: #718096;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 3px;
margin-bottom: 5px;
}
.info-line {
font-size: 9px;
color: #2d3748;
margin-bottom: 2px;
line-height: 1.5;
}
/* ── STATUS BADGE ── */
.badge {
display: inline;
padding: 2px 6px;
font-size: 8px;
font-weight: bold;
text-transform: uppercase;
border-radius: 3px;
}
.badge-paid { background: #c6f6d5; color: #22543d; }
.badge-pending { background: #fef3c7; color: #92400e; }
.badge-paying { background: #dbeafe; color: #1e40af; }
.badge-overdue { background: #fee2e2; color: #991b1b; }
/* ── BILL TO ── */
.bill-to-box {
border: 1px solid #cbd5e0;
padding: 8px 12px;
margin-bottom: 10px;
background: #f7fafc;
}
.bill-to-label {
font-size: 8px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1px;
color: #718096;
margin-bottom: 4px;
}
.bill-name { font-size: 12px; font-weight: bold; color: #1a202c; margin-bottom: 2px; }
.bill-sub { font-size: 10px; font-weight: bold; color: #2d3748; margin-bottom: 2px; }
.bill-detail { font-size: 9px; color: #4a5568; line-height: 1.8; }
/* ── SUMMARY GRID — 4 boxes per row ── */
.summary-wrap { margin-bottom: 12px; }
.summary-label {
font-size: 9px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1px;
color: #4a5568;
margin-bottom: 4px;
}
.summary-outer {
width: 100%;
border-collapse: collapse;
}
.sbox {
width: 25%;
border: 1px solid #cbd5e0;
padding: 7px 8px;
text-align: center; text-align: center;
vertical-align: middle;
} }
.footer strong { .sbox-row1 { background: #ffffff; }
color: #222; .sbox-row2 { background: #f7fafc; }
font-weight: 500; .sbox-lbl {
font-size: 7px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1px;
color: #718096;
margin-bottom: 3px;
}
.sbox-val {
font-size: 11px;
font-weight: bold;
color: #1a202c;
}
.sbox-sub {
font-size: 7px;
color: #a0aec0;
margin-top: 1px;
}
/* ── CHARGE GROUPS ── */
.cg-header {
font-size: 10px;
font-weight: bold;
color: #ffffff;
background: #2d3748;
padding: 5px 10px;
text-transform: uppercase;
letter-spacing: 1px;
}
.cg-group-wrap {
border: 1px solid #cbd5e0;
margin-bottom: 10px;
}
.cg-sum-table {
width: 100%;
border-collapse: collapse;
font-size: 9px;
}
.cg-sum-table th {
background: #edf2f7;
padding: 5px;
font-size: 8px;
font-weight: bold;
text-transform: uppercase;
color: #4a5568;
border: 1px solid #cbd5e0;
text-align: left;
}
.cg-sum-table td {
padding: 5px;
border: 1px solid #cbd5e0;
color: #2d3748;
font-size: 9px;
}
.cg-item-table {
width: 100%;
border-collapse: collapse;
font-size: 8px;
}
.cg-item-table th {
background: #f0fff4;
padding: 4px 5px;
font-size: 7px;
font-weight: bold;
text-transform: uppercase;
color: #276749;
border: 1px solid #c6f6d5;
text-align: left;
white-space: nowrap;
}
.cg-item-table td {
padding: 4px 5px;
border: 1px solid #e2e8f0;
color: #2d3748;
white-space: nowrap;
}
.row-even { background: #f7fafc; }
/* ── GRAND TOTAL ── */
.grand-outer {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
margin-bottom: 10px;
}
.grand-spacer { width: 60%; }
.grand-inner { width: 40%; vertical-align: top; }
.grand-table { width: 100%; border-collapse: collapse; font-size: 9px; }
.grand-table td { padding: 4px 8px; border: 1px solid #e2e8f0; }
.g-lbl { text-align: right; font-weight: bold; color: #4a5568; background: #f7fafc; }
.g-val { text-align: right; font-weight: bold; color: #1a202c; }
.g-total-lbl { text-align: right; font-size: 11px; font-weight: bold; color: #1a202c; background: #ebf8ff; border-top: 2px solid #2b6cb0; }
.g-total-val { text-align: right; font-size: 11px; font-weight: bold; color: #1a202c; background: #ebf8ff; border-top: 2px solid #2b6cb0; }
/* ── INSTALLMENTS ── */
.install-section { margin-top: 12px; }
.install-header {
font-size: 9px;
font-weight: bold;
color: #fff;
background: #2d3748;
padding: 5px 10px;
text-transform: uppercase;
letter-spacing: 1px;
}
.install-table { width: 100%; border-collapse: collapse; font-size: 9px; }
.install-table th {
background: #edf2f7;
padding: 5px 6px;
font-size: 8px;
font-weight: bold;
text-transform: uppercase;
color: #4a5568;
border: 1px solid #cbd5e0;
}
.install-table td { padding: 4px 6px; border: 1px solid #e2e8f0; color: #2d3748; }
/* ── HELPERS ── */
.tr { text-align: right; }
.tc { text-align: center; }
.bold { font-weight: bold; }
/* ── FOOTER ── */
.footer {
margin-top: 16px;
padding-top: 8px;
border-top: 1px solid #e2e8f0;
font-size: 8px;
color: #718096;
text-align: center;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container">
<!-- Header Section --> {{-- ══ VARIABLES ══ --}}
<div class="header"> @php
<div class="logo-company"> $companyName = $invoice->company_name ?: ($invoice->customer->company_name ?? null);
<img src="{{ public_path('images/kent_logo2.png') }}" class="logo"> $custName = $invoice->customer_name;
<div class="company-details"> $custAddress = $invoice->customer_address ?: ($invoice->customer->address ?? null);
<div class="company-title">{{ $invoice->company_name ?? 'Kent International Pvt. Ltd.' }}</div> $custPincode = $invoice->pincode ?: ($invoice->customer->pincode ?? null);
<div class="company-sub"></div> $custEmail = $invoice->customer_email ?: ($invoice->customer->email ?? null);
{{ $invoice->company_address ?? '123 Business Park, Sector 5' }}<br> $custMobile = $invoice->customer_mobile ?: ($invoice->customer->mobile_no ?? null);
{{ $invoice->company_city ?? 'Gurugram, Haryana 122001' }}<br> @endphp
{{ $invoice->company_country ?? 'India' }}<br>
GST: {{ $invoice->company_gst ?? 'GST123456789' }}<br> {{-- ══ 1) HEADER Logo + Company Name ══ --}}
Email: {{ $invoice->company_email ?? 'billing@kent.com' }}<br> <div class="top-header">
Phone: {{ $invoice->company_phone ?? '+91 124 123 4567' }} <table class="header-table">
<tr>
{{-- Logo: 55x55px DomPDF साठी public_path() वापरतो --}}
<td class="logo-cell">
<img src="{{ public_path('images/kent_logo2.png') }}" alt="Logo" width="55" height="55">
</td>
{{-- Company Name + Subtitle + Tagline --}}
<td class="title-cell">
<div class="company-name">KENT</div>
<div class="company-subtitle">International Pvt. Ltd.</div>
<div class="company-tagline">
Address Line 1, City, State Pincode &nbsp;|&nbsp;
Email: info@company.com &nbsp;|&nbsp;
Phone: +91 1234567890
</div> </div>
</td>
</tr>
</table>
</div>
{{-- ══ 2) CUSTOMER | INVOICE ══ --}}
<table class="info-outer">
<tr>
<td class="info-left">
<div class="section-title">Customer Details</div>
@if($companyName)
<div class="info-line bold" style="font-size:11px;">{{ $companyName }}</div>
<div class="info-line">{{ $custName }}</div>
@else
<div class="info-line bold" style="font-size:11px;">{{ $custName }}</div>
@endif
@if($custAddress)<div class="info-line">{{ $custAddress }}</div>@endif
@if($custPincode)<div class="info-line">{{ $custPincode }}</div>@endif
@if($custEmail)<div class="info-line">Email: {{ $custEmail }}</div>@endif
@if($custMobile)<div class="info-line">Phone: {{ $custMobile }}</div>@endif
</td>
<td class="info-right">
<div class="section-title">Invoice Details</div>
<div class="info-line"><strong>Invoice #:</strong> {{ $invoice->invoice_number }}</div>
<div class="info-line"><strong>Date:</strong> {{ \Carbon\Carbon::parse($invoice->invoice_date)->format('d M, Y') }}</div>
<div class="info-line"><strong>Due Date:</strong> {{ \Carbon\Carbon::parse($invoice->due_date)->format('d M, Y') }}</div>
<div class="info-line">
<strong>Status:</strong>
<span class="badge badge-{{ strtolower($invoice->status) }}">{{ strtoupper($invoice->status) }}</span>
</div> </div>
<div class="invoice-details"> <div class="info-line"><strong>GST Type:</strong> {{ strtoupper($invoice->tax_type ?? 'GST') }}</div>
<div class="invoice-title">INVOICE</div> <div class="info-line"><strong>GST %:</strong> {{ number_format($invoice->gst_percent ?? 0, 2) }}%</div>
Invoice #: {{ $invoice->invoice_number }}<br> </td>
Issue Date: {{ date('d M Y', strtotime($invoice->invoice_date)) }}<br> </tr>
Due Date: {{ date('d M Y', strtotime($invoice->due_date)) }} </table>
@if(strtolower($invoice->status) == 'paid')
<div class="paid-label"> {{-- ══ 3) BILL TO ══ --}}
<span class="paid-tag">Paid</span> <div class="bill-to-box">
</div> <div class="bill-to-label">Bill To</div>
<div class="paid-date"> @if($companyName)
Paid on: {{ date('d M Y', strtotime($invoice->paid_date ?? now())) }} <div class="bill-name">{{ $companyName }}</div>
</div> <div class="bill-sub">{{ $custName }}</div>
@else @else
<div class="paid-date" style="color:#d00;">Status: {{ ucfirst($invoice->status) }}</div> <div class="bill-name">{{ $custName }}</div>
@endif @endif
</div> <div class="bill-detail">
</div> @if($custAddress){{ $custAddress }}<br>@endif
<!-- Bill To Section --> @if($custPincode){{ $custPincode }}<br>@endif
<div class="bill-section"> @if($custEmail)Email: {{ $custEmail }}<br>@endif
<div class="bill-title">Bill To</div> @if($custMobile)Phone: {{ $custMobile }}@endif
<div class="bill-details">
<strong>{{ $invoice->customer_name }}</strong><br>
{{ $invoice->customer_address }}<br>
GST: {{ $invoice->customer_gst ?? '-' }}<br>
Email: {{ $invoice->customer_email }}<br>
Phone: {{ $invoice->customer_mobile }}
</div>
</div>
<!-- Items Table -->
<table>
<thead>
<tr>
<th>Description</th>
<th>Qty</th>
<th>Rate ()</th>
<th>Amount ()</th>
</tr>
</thead>
<tbody>
@foreach($invoice->items as $item)
<tr>
<td>{{ $item->description }}</td>
<td>{{ $item->qty }}</td>
<td>{{ number_format($item->price, 0) }}</td>
<td>{{ number_format($item->ttl_amount, 0) }}</td>
</tr>
@endforeach
<tr class="totals-row">
<td colspan="3" style="text-align:right;">Subtotal:</td>
<td>{{ number_format($invoice->subtotal, 0) }}</td>
</tr>
<tr class="gst-row">
<td colspan="3" style="text-align:right;">GST ({{ $invoice->gst_percent }}%):</td>
<td>{{ number_format($invoice->gst_amount, 0) }}</td>
</tr>
<tr class="total-row">
<td colspan="3" style="text-align:right;">Total:</td>
<td>{{ number_format($invoice->final_amount_with_gst, 0) }}</td>
</tr>
</tbody>
</table>
<!-- Payment Info & Reference -->
<div class="payment-info">
<strong>Payment Method:</strong> {{ $invoice->payment_method ?? 'Bank Transfer' }}
</div>
<div class="ref-number">
Reference Number: {{ $invoice->reference_no ?? "REF123456789" }}
</div>
<!-- Footer -->
<div class="footer">
Thank you for your business!<br>
For any queries, please contact us at <strong>{{ $invoice->company_email ?? 'billing@kent.com' }}</strong>
</div>
</div> </div>
</div>
{{-- ══ 4) 8 SUMMARY BOXES ══ --}}
@php
$allGroupItems = $invoice->chargeGroups->flatMap(fn($g) => $g->items);
$invoiceItemIds = $allGroupItems->pluck('invoice_item_id')->unique()->values();
$invoiceItems = $invoice->items->whereIn('id', $invoiceItemIds);
$totalMarkCount = $invoiceItems->pluck('mark_no')->filter()->unique()->count();
$totalCtn = $invoiceItems->sum('ctn');
$totalQty = $invoiceItems->sum('ttl_qty');
$totalCbm = $invoiceItems->sum('ttl_cbm');
$totalKg = $invoiceItems->sum('ttl_kg');
@endphp
<div class="summary-wrap">
<div class="summary-label">Container &amp; Shipment Summary</div>
<table class="summary-outer">
<tr>
<td class="sbox sbox-row1">
<div class="sbox-lbl">Container Name</div>
<div class="sbox-val">{{ $invoice->container->container_name ?? '—' }}</div>
</td>
<td class="sbox sbox-row1">
<div class="sbox-lbl">Container Date</div>
<div class="sbox-val">
@if($invoice->container && $invoice->container->container_date)
{{ \Carbon\Carbon::parse($invoice->container->container_date)->format('d M, Y') }}
@else@endif
</div>
</td>
<td class="sbox sbox-row1">
<div class="sbox-lbl">Container No.</div>
<div class="sbox-val">{{ $invoice->container->container_number ?? '—' }}</div>
</td>
<td class="sbox sbox-row1">
<div class="sbox-lbl">Total Mark No.</div>
<div class="sbox-val">{{ $totalMarkCount }}</div>
<div class="sbox-sub">Unique marks</div>
</td>
</tr>
<tr>
<td class="sbox sbox-row2">
<div class="sbox-lbl">Total CTN</div>
<div class="sbox-val">{{ number_format($totalCtn, 0) }}</div>
<div class="sbox-sub">Cartons</div>
</td>
<td class="sbox sbox-row2">
<div class="sbox-lbl">Total QTY</div>
<div class="sbox-val">{{ number_format($totalQty, 0) }}</div>
<div class="sbox-sub">Pieces</div>
</td>
<td class="sbox sbox-row2">
<div class="sbox-lbl">Total CBM</div>
<div class="sbox-val">{{ number_format($totalCbm, 3) }}</div>
<div class="sbox-sub">Cubic Meter</div>
</td>
<td class="sbox sbox-row2">
<div class="sbox-lbl">Total KG</div>
<div class="sbox-val">{{ number_format($totalKg, 2) }}</div>
<div class="sbox-sub">Kilograms</div>
</td>
</tr>
</table>
</div>
{{-- ══ 5) CHARGE GROUPS ══ --}}
@if($invoice->chargeGroups && $invoice->chargeGroups->count() > 0)
<div style="margin-bottom:14px;">
<div class="cg-header">Charge Groups</div>
@foreach($invoice->chargeGroups as $group)
@php
$groupItemIds = $group->items->pluck('invoice_item_id')->toArray();
$groupInvItems = $invoice->items->whereIn('id', $groupItemIds);
@endphp
<div class="cg-group-wrap">
<table class="cg-sum-table">
<thead>
<tr>
<th style="width:22%;">Group Name</th>
<th style="width:11%;">Basis</th>
<th style="width:11%;">Basis Value</th>
<th style="width:10%;">Rate</th>
<th style="width:13%;" class="tr">Total Charge</th>
<th style="width:8%;">GST %</th>
<th style="width:9%;">Tax Type</th>
<th style="width:16%;" class="tr">Total With GST</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>{{ $group->group_name ?? 'Group '.$group->id }}</strong></td>
<td>{{ strtoupper($group->basis_type) }}</td>
<td>{{ number_format($group->basis_value, 3) }}</td>
<td>{{ number_format($group->rate, 2) }}</td>
<td class="tr">{{ number_format($group->total_charge, 2) }}</td>
<td>{{ number_format($group->gst_percent ?? 0, 2) }}%</td>
<td>{{ strtoupper($group->tax_type ?? 'GST') }}</td>
<td class="tr"><strong>{{ number_format($group->total_with_gst, 2) }}</strong></td>
</tr>
</tbody>
</table>
@if($groupInvItems->count() > 0)
<table class="cg-item-table">
<thead>
<tr>
<th style="width:3%;">#</th>
<th style="width:15%;">Description</th>
<th style="width:7%;">Mark No</th>
<th style="width:5%;" class="tc">QTY</th>
<th style="width:6%;" class="tr">TTL QTY</th>
<th style="width:6%;" class="tr">CBM</th>
<th style="width:6%;" class="tr">TTL CBM</th>
<th style="width:5%;" class="tr">KG</th>
<th style="width:6%;" class="tr">TTL KG</th>
<th style="width:7%;" class="tr">TTL Amt</th>
<th style="width:5%;" class="tr">Rate</th>
<th style="width:7%;" class="tr">Total Charge</th>
<th style="width:5%;" class="tr">GST %</th>
<th style="width:6%;" class="tr">Item GST</th>
<th style="width:6%;" class="tr">Item Total</th>
</tr>
</thead>
<tbody>
@foreach($groupInvItems as $idx => $item)
@php
$groupBasisTotal = $groupInvItems->sum(function($i) use ($group) {
return match($group->basis_type) {
'ttl_qty' => $i->ttl_qty ?? 0,
'ttl_cbm' => $i->ttl_cbm ?? 0,
'ttl_kg' => $i->ttl_kg ?? 0,
'amount' => $i->ttl_amount ?? 0,
default => $i->ttl_amount ?? 0,
};
});
$itemBasisValue = match($group->basis_type) {
'ttl_qty' => $item->ttl_qty ?? 0,
'ttl_cbm' => $item->ttl_cbm ?? 0,
'ttl_kg' => $item->ttl_kg ?? 0,
'amount' => $item->ttl_amount ?? 0,
default => $item->ttl_amount ?? 0,
};
$itemCharge = $groupBasisTotal > 0
? ($itemBasisValue / $groupBasisTotal) * $group->total_charge
: 0;
$gstPct = $group->gst_percent ?? 0;
$itemGst = $itemCharge * $gstPct / 100;
$itemTotal = $itemCharge + $itemGst;
@endphp
<tr class="{{ $idx % 2 === 1 ? 'row-even' : '' }}">
<td class="tc">{{ $idx + 1 }}</td>
<td>{{ $item->description }}</td>
<td>{{ $item->mark_no ?? '—' }}</td>
<td class="tc">{{ $item->qty ?? 0 }}</td>
<td class="tr">{{ number_format($item->ttl_qty ?? 0, 0) }}</td>
<td class="tr">{{ number_format($item->cbm ?? 0, 3) }}</td>
<td class="tr">{{ number_format($item->ttl_cbm ?? 0, 3) }}</td>
<td class="tr">{{ number_format($item->kg ?? 0, 2) }}</td>
<td class="tr">{{ number_format($item->ttl_kg ?? 0, 2) }}</td>
<td class="tr">{{ number_format($item->ttl_amount ?? 0, 2) }}</td>
<td class="tr">{{ number_format($group->rate, 2) }}</td>
<td class="tr">{{ number_format($itemCharge, 2) }}</td>
<td class="tr">{{ number_format($gstPct, 2) }}%</td>
<td class="tr">{{ number_format($itemGst, 2) }}</td>
<td class="tr"><strong>{{ number_format($itemTotal, 2) }}</strong></td>
</tr>
@endforeach
</tbody>
</table>
@endif
</div>
@endforeach
</div>
@endif
{{-- ══ GRAND TOTAL ══ --}}
<table class="grand-outer">
<tr>
<td class="grand-spacer"></td>
<td class="grand-inner">
<table class="grand-table">
<tr>
<td class="g-lbl">Charge Groups Total:</td>
<td class="g-val">{{ number_format($invoice->charge_groups_total ?? 0, 2) }}</td>
</tr>
<tr>
<td class="g-lbl">GST Amount ({{ number_format($invoice->gst_percent ?? 0, 2) }}%):</td>
<td class="g-val">{{ number_format($invoice->gst_amount ?? 0, 2) }}</td>
</tr>
<tr>
<td class="g-total-lbl">Grand Total:</td>
<td class="g-total-val">{{ number_format($invoice->grand_total_with_charges ?? 0, 2) }}</td>
</tr>
</table>
</td>
</tr>
</table>
{{-- ══ INSTALLMENTS ══ --}}
@if($invoice->installments && $invoice->installments->count() > 0)
<div class="install-section">
<div class="install-header">Payment Installments</div>
<table class="install-table">
<thead>
<tr>
<th style="width:6%;" class="tc">#</th>
<th style="width:20%;">Date</th>
<th style="width:25%;">Payment Method</th>
<th style="width:25%;">Reference No</th>
<th style="width:24%;" class="tr">Amount ()</th>
</tr>
</thead>
<tbody>
@foreach($invoice->installments as $idx => $inst)
<tr>
<td class="tc">{{ $idx + 1 }}</td>
<td>{{ \Carbon\Carbon::parse($inst->installment_date)->format('d M, Y') }}</td>
<td>{{ strtoupper($inst->payment_method) }}</td>
<td>{{ $inst->reference_no ?? '—' }}</td>
<td class="tr">{{ number_format($inst->amount, 2) }}</td>
</tr>
@endforeach
</tbody>
</table>
@php
$totalPaid = $invoice->installments->sum('amount');
$grandTotal = $invoice->grand_total_with_charges ?? 0;
$remaining = max(0, $grandTotal - $totalPaid);
@endphp
<table class="grand-outer" style="margin-top:6px;">
<tr>
<td class="grand-spacer"></td>
<td class="grand-inner">
<table class="grand-table">
<tr>
<td class="g-lbl" style="color:#059669;">Total Paid:</td>
<td class="g-val" style="color:#059669;">{{ number_format($totalPaid, 2) }}</td>
</tr>
<tr>
<td class="g-lbl" style="color:#dc2626;">Remaining:</td>
<td class="g-val" style="color:#dc2626;">{{ number_format($remaining, 2) }}</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
@endif
{{-- ══ FOOTER ══ --}}
<div class="footer">
Thank you for your business! &nbsp;|&nbsp; For queries: info@company.com &nbsp;|&nbsp; +91 1234567890
</div>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -71,7 +71,19 @@ Route::prefix('admin')
Route::get('/dashboard', [AdminOrderController::class, 'dashboard']) Route::get('/dashboard', [AdminOrderController::class, 'dashboard'])
->name('admin.dashboard'); ->name('admin.dashboard');
Route::get('/reports', [AdminReportController::class, 'index'])->name('admin.reports'); Route::get('/reports/containers/excel', [AdminReportController::class, 'containerReportExcel'])
->name('admin.reports.containers.excel');
Route::get('/reports/containers/pdf', [AdminReportController::class, 'containerReportPdf'])
->name('admin.reports.containers.pdf');
//reports DOWNLOAD
Route::get('/reports', [AdminReportController::class, 'containerReport'])
->name('admin.reports');
Route::get('/chat-support', fn() => view('admin.chat_support')) Route::get('/chat-support', fn() => view('admin.chat_support'))
->name('admin.chat_support'); ->name('admin.chat_support');
@@ -82,8 +94,7 @@ Route::prefix('admin')
Route::get('/account', fn() => view('admin.account')) Route::get('/account', fn() => view('admin.account'))
->name('admin.account'); ->name('admin.account');
Route::get('/profile', fn() => view('admin.profile')) Route::get('/profile', [AdminAuthController::class, 'profile'])->name('admin.profile');
->name('admin.profile');
Route::post( Route::post(
'admin/orders/upload-excel-preview', 'admin/orders/upload-excel-preview',
@@ -378,3 +389,16 @@ Route::get('/admin/containers/{container}/popup', [\App\Http\Controllers\Contain
->name('containers.popup'); ->name('containers.popup');
//Invoice pdf download route
Route::get('invoice/{id}/download-pdf', [AdminInvoiceController::class, 'downloadPdf'])
->name('admin.invoice.download.pdf');
// Route::get('invoice/{id}/download-excel', [AdminInvoiceController::class, 'downloadExcel'])
// ->name('admin.invoice.download.excel');
Route::get('invoice/{id}/share', [AdminInvoiceController::class, 'share'])
->name('admin.invoice.share');