Compare commits

..

18 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
Utkarsh Khedkar
d5e9113820 Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2026-03-09 12:28:11 +05:30
Utkarsh Khedkar
bddbcf5c5f logo changes 2026-03-09 12:27:42 +05:30
Abhishek Mali
0c51ed1489 mark list and custumer data update 2026-03-09 12:04:08 +05:30
Utkarsh Khedkar
9cc6959396 Pdf Changes Done 2026-03-09 10:24:44 +05:30
Utkarsh Khedkar
c11467068c Frontend Changes 2026-02-28 11:00:48 +05:30
Abhishek Mali
599023166a update staff permissions 2026-02-27 12:59:20 +05:30
Utkarsh Khedkar
e188780329 All Kent Code Updated 2026-02-27 10:51:26 +05:30
134 changed files with 13671 additions and 8423 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
@@ -20,12 +20,12 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
DB_CONNECTION=sqlite DB_CONNECTION=mysql
# DB_HOST=127.0.0.1 DB_HOST=127.0.0.1
# DB_PORT=3306 DB_PORT=3306
# DB_DATABASE=laravel DB_DATABASE=kent_logistics6
# DB_USERNAME=root DB_USERNAME=root
# DB_PASSWORD= DB_PASSWORD=
SESSION_DRIVER=database SESSION_DRIVER=database
SESSION_LIFETIME=120 SESSION_LIFETIME=120

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Exports;
use Illuminate\Contracts\View\View;
use Maatwebsite\Excel\Concerns\FromView;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class InvoicesExport implements FromView
{
protected $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function view(): View
{
$request = $this->request;
$invoices = DB::table('invoices')
->leftJoin('containers', 'containers.id', '=', 'invoices.container_id')
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
->select(
'invoices.invoice_number',
'invoices.invoice_date',
'invoices.mark_no',
'containers.container_number',
'containers.container_date',
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name'),
'invoices.final_amount',
'invoices.final_amount_with_gst',
'invoices.status as invoice_status'
)
->when($request->filled('search'), function ($q) use ($request) {
$search = trim($request->search);
$q->where(function ($qq) use ($search) {
$qq->where('invoices.invoice_number', 'like', "%{$search}%")
->orWhere('invoices.mark_no', 'like', "%{$search}%")
->orWhere('containers.container_number', 'like', "%{$search}%")
->orWhere('mark_list.company_name', 'like', "%{$search}%")
->orWhere('mark_list.customer_name', 'like', "%{$search}%");
});
})
->when($request->filled('status'), function ($q) use ($request) {
$q->where('invoices.status', $request->status);
})
->when($request->filled('from_date'), function ($q) use ($request) {
$q->whereDate('invoices.invoice_date', '>=', $request->from_date);
})
->when($request->filled('to_date'), function ($q) use ($request) {
$q->whereDate('invoices.invoice_date', '<=', $request->to_date);
})
->orderByDesc('containers.container_date')
->orderByDesc('invoices.id')
->get();
return view('admin.pdf.invoices_excel', compact('invoices'));
}
}

View File

@@ -5,6 +5,10 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use 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
{ {
@@ -35,12 +39,11 @@ class AdminAuthController extends Controller
'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,38 +2,70 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller; // ⬅️ हे नक्की असू दे
use Illuminate\Http\Request;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\InvoiceItem; use App\Models\InvoiceItem;
use Mpdf\Mpdf; use App\Models\InvoiceChargeGroup;
use App\Models\InvoiceChargeGroupItem;
use App\Models\InvoiceInstallment; use App\Models\InvoiceInstallment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Barryvdh\DomPDF\Facade\Pdf; use Mpdf\Mpdf;
class AdminInvoiceController extends Controller class AdminInvoiceController extends Controller
{ {
// ------------------------------------------------------------- // -------------------------------------------------------------
// INVOICE LIST PAGE // INDEX (LIST ALL INVOICES WITH FILTERS)
// ------------------------------------------------------------- // -------------------------------------------------------------
public function index() public function index(Request $request)
{ {
$invoices = Invoice::with(['order.shipments'])->latest()->get(); $query = Invoice::query();
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('invoice_number', 'like', "%{$search}%")
->orWhere('customer_name', 'like', "%{$search}%");
});
}
if ($request->filled('status') && $request->status !== 'all') {
$query->where('status', $request->status);
}
if ($request->filled('start_date')) {
$query->whereDate('invoice_date', '>=', $request->start_date);
}
if ($request->filled('end_date')) {
$query->whereDate('invoice_date', '<=', $request->end_date);
}
$invoices = $query->latest()->get();
return view('admin.invoice', compact('invoices')); return view('admin.invoice', compact('invoices'));
} }
// ------------------------------------------------------------- // -------------------------------------------------------------
// POPUP VIEW (AJAX) // POPUP VIEW
// ------------------------------------------------------------- // -------------------------------------------------------------
public function popup($id) public function popup($id)
{ {
$invoice = Invoice::with(['items', 'order', 'installments'])->findOrFail($id); $invoice = Invoice::with([
'items',
'chargeGroups.items',
])->findOrFail($id);
$shipment = \App\Models\Shipment::whereHas('items', function ($q) use ($invoice) { $shipment = null;
$q->where('order_id', $invoice->order_id);
})->first();
return view('admin.popup_invoice', compact('invoice', 'shipment')); $groupedItemIds = $invoice->chargeGroups
->flatMap(fn($group) => $group->items->pluck('invoice_item_id'))
->unique()
->values()
->toArray();
return view('admin.popup_invoice', compact('invoice', 'shipment', 'groupedItemIds'));
} }
// ------------------------------------------------------------- // -------------------------------------------------------------
@@ -41,27 +73,55 @@ class AdminInvoiceController extends Controller
// ------------------------------------------------------------- // -------------------------------------------------------------
public function edit($id) public function edit($id)
{ {
$invoice = Invoice::with(['order.shipments'])->findOrFail($id); $invoice = Invoice::with([
$shipment = $invoice->order?->shipments?->first(); 'items',
'customer',
'container',
'chargeGroups.items',
'installments',
])->findOrFail($id);
// ADD THIS SECTION: Calculate customer's total due across all invoices // ✅ Customer details sync
$customerTotalDue = Invoice::where('customer_id', $invoice->customer_id) if ($invoice->customer) {
->where('status', '!=', 'cancelled') $needsUpdate = [];
->where('status', '!=', 'void')
->sum('final_amount_with_gst');
// Pass the new variable to the view if (empty($invoice->customer_email) || $invoice->customer_email === 'test@demo.com') {
return view('admin.invoice_edit', compact('invoice', 'shipment', 'customerTotalDue')); $needsUpdate['customer_email'] = $invoice->customer->email;
}
if (empty($invoice->customer_address) || $invoice->customer_address === 'TEST ADDRESS') {
$needsUpdate['customer_address'] = $invoice->customer->address;
}
if (empty($invoice->pincode) || $invoice->pincode === '999999') {
$needsUpdate['pincode'] = $invoice->customer->pincode;
}
if (!empty($needsUpdate)) {
$invoice->update($needsUpdate);
$invoice->refresh();
}
}
$shipment = null;
$groupedItemIds = $invoice->chargeGroups
->flatMap(function ($group) {
return $group->items->pluck('invoice_item_id');
})
->unique()
->values()
->toArray();
return view('admin.invoice_edit', compact('invoice', 'shipment', 'groupedItemIds'));
} }
// ------------------------------------------------------------- // -------------------------------------------------------------
// UPDATE INVOICE // UPDATE INVOICE (HEADER ONLY)
// ------------------------------------------------------------- // -------------------------------------------------------------
public function update(Request $request, $id) 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);
@@ -69,69 +129,79 @@ class AdminInvoiceController extends Controller
$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',
'tax_percent' => 'required|numeric|min:0|max:28',
'status' => 'required|in:pending,paid,overdue',
'notes' => 'nullable|string', 'notes' => 'nullable|string',
]); ]);
Log::info("✅ Validated Invoice Update Data", $data); Log::info('✅ Validated Invoice Header Update Data', $data);
$finalAmount = floatval($data['final_amount']);
$taxPercent = floatval($data['tax_percent']);
$taxAmount = 0;
if ($data['tax_type'] === 'gst') {
Log::info("🟢 GST Selected", compact('taxPercent'));
$data['cgst_percent'] = $taxPercent / 2;
$data['sgst_percent'] = $taxPercent / 2;
$data['igst_percent'] = 0;
} else {
Log::info("🔵 IGST Selected", compact('taxPercent'));
$data['cgst_percent'] = 0;
$data['sgst_percent'] = 0;
$data['igst_percent'] = $taxPercent;
}
$taxAmount = ($finalAmount * $taxPercent) / 100;
$data['gst_amount'] = $taxAmount;
$data['final_amount_with_gst'] = $finalAmount + $taxAmount;
$data['gst_percent'] = $taxPercent;
Log::info("📌 Final Calculated Invoice Values", [
'invoice_id' => $invoice->id,
'final_amount' => $finalAmount,
'gst_amount' => $data['gst_amount'],
'final_amount_with_gst' => $data['final_amount_with_gst'],
'tax_type' => $data['tax_type'],
'cgst_percent' => $data['cgst_percent'],
'sgst_percent' => $data['sgst_percent'],
'igst_percent' => $data['igst_percent'],
]);
$invoice->update($data); $invoice->update($data);
$invoice->refresh();
Log::info(" Invoice Updated Successfully", [ Log::info('🔍 Invoice AFTER HEADER UPDATE', [
'invoice_id' => $invoice->id 'invoice_id' => $invoice->id,
'charge_groups_total' => $invoice->charge_groups_total,
'gst_amount' => $invoice->gst_amount,
'grand_total_with_charges'=> $invoice->grand_total_with_charges,
]); ]);
// regenerate PDF
$this->generateInvoicePDF($invoice); $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.');
} }
// ------------------------------------------------------------- // -------------------------------------------------------------
// PDF GENERATION USING mPDF // UPDATE INVOICE ITEMS (फक्त items save)
// -------------------------------------------------------------
public function updateItems(Request $request, Invoice $invoice)
{
Log::info('🟡 Invoice Items Update Request', [
'invoice_id' => $invoice->id,
'payload' => $request->all(),
]);
$data = $request->validate([
'items' => ['required', 'array'],
'items.*.price' => ['required', 'numeric', 'min:0'],
'items.*.ttl_amount' => ['required', 'numeric', 'min:0'],
]);
foreach ($data['items'] as $itemId => $itemData) {
$item = InvoiceItem::where('id', $itemId)
->where('invoice_id', $invoice->id)
->first();
if (!$item) {
Log::warning('Invoice item not found or mismatched invoice', [
'invoice_id' => $invoice->id,
'item_id' => $itemId,
]);
continue;
}
$item->price = $itemData['price'];
$item->ttl_amount = $itemData['ttl_amount'];
$item->save();
}
Log::info('✅ Invoice items updated (no totals recalculation)', [
'invoice_id' => $invoice->id,
]);
return back()->with('success', 'Invoice items updated successfully.');
}
// -------------------------------------------------------------
// PDF GENERATION (EXISTING - केवळ chargeGroups load ला confirm कर)
// ------------------------------------------------------------- // -------------------------------------------------------------
public function generateInvoicePDF($invoice) public function generateInvoicePDF($invoice)
{ {
$invoice->load(['items', 'order.shipments']); // ✅ यामध्ये chargeGroups आणि installments load कर
$shipment = $invoice->order?->shipments?->first(); $invoice->load(['items', 'customer', 'container', 'chargeGroups.items', 'installments']);
$shipment = null;
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf'; $fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
$folder = public_path('invoices/'); $folder = public_path('invoices/');
@@ -140,30 +210,30 @@ class AdminInvoiceController extends Controller
} }
$filePath = $folder . $fileName; $filePath = $folder . $fileName;
if (file_exists($filePath)) { if (file_exists($filePath)) {
unlink($filePath); unlink($filePath);
} }
$mpdf = new Mpdf(['mode' => 'utf-8', 'format' => 'A4', 'default_font' => 'sans-serif']); $mpdf = new Mpdf([
$html = view('admin.pdf.invoice', ['invoice' => $invoice, 'shipment' => $shipment])->render(); 'mode' => 'utf-8',
'format' => 'A4',
'default_font' => 'sans-serif',
]);
$html = view('admin.pdf.invoice', [
'invoice' => $invoice,
'shipment' => $shipment,
])->render();
$mpdf->WriteHTML($html); $mpdf->WriteHTML($html);
$mpdf->Output($filePath, 'F'); $mpdf->Output($filePath, 'F');
$invoice->update(['pdf_path' => 'invoices/' . $fileName]); $invoice->update(['pdf_path' => 'invoices/' . $fileName]);
} }
public function downloadInvoice($id)
{
$invoice = Invoice::findOrFail($id);
// ALWAYS regenerate to reflect latest HTML/CSS
$this->generateInvoicePDF($invoice);
$invoice->refresh();
return response()->download(public_path($invoice->pdf_path));
}
// ------------------------------------------------------------- // -------------------------------------------------------------
// INSTALLMENTS (ADD/DELETE) // INSTALLMENTS (ADD)
// ------------------------------------------------------------- // -------------------------------------------------------------
public function storeInstallment(Request $request, $invoice_id) public function storeInstallment(Request $request, $invoice_id)
{ {
@@ -176,14 +246,15 @@ class AdminInvoiceController extends Controller
$invoice = Invoice::findOrFail($invoice_id); $invoice = Invoice::findOrFail($invoice_id);
$grandTotal = $invoice->grand_total_with_charges ?? 0;
$paidTotal = $invoice->installments()->sum('amount'); $paidTotal = $invoice->installments()->sum('amount');
// Use GST-inclusive total for all calculations/checks $remaining = $grandTotal - $paidTotal;
$remaining = $invoice->final_amount_with_gst - $paidTotal;
if ($request->amount > $remaining) { if ($request->amount > $remaining) {
return response()->json([ return response()->json([
'status' => 'error', 'status' => 'error',
'message' => 'Installment amount exceeds remaining balance.' 'message' => 'Installment amount exceeds remaining balance.',
], 422); ], 422);
} }
@@ -196,53 +267,238 @@ class AdminInvoiceController extends Controller
]); ]);
$newPaid = $paidTotal + $request->amount; $newPaid = $paidTotal + $request->amount;
$remaining = max(0, $grandTotal - $newPaid);
// Mark as 'paid' if GST-inclusive total is cleared $isOverdue = now()->startOfDay()->gt(\Carbon\Carbon::parse($invoice->due_date)->startOfDay());
if ($newPaid >= $invoice->final_amount_with_gst) {
$invoice->update(['status' => 'paid']);
$this->generateInvoicePDF($invoice); if ($grandTotal > 0 && $newPaid >= $grandTotal) {
$newStatus = 'paid';
} elseif ($newPaid > 0 && $isOverdue) {
$newStatus = 'overdue';
} elseif ($newPaid > 0 && !$isOverdue) {
$newStatus = 'paying';
} elseif ($newPaid <= 0 && $isOverdue) {
$newStatus = 'overdue';
} else {
$newStatus = 'pending';
} }
$invoice->update([
'payment_method' => $request->payment_method,
'reference_no' => $request->reference_no,
'status' => $newStatus,
]);
return response()->json([ return response()->json([
'status' => 'success', 'status' => 'success',
'message' => 'Installment added successfully.', 'message' => 'Installment added successfully.',
'installment' => $installment, 'installment' => $installment,
'chargeGroupsTotal' => $invoice->charge_groups_total ?? 0,
'gstAmount' => $invoice->gst_amount ?? 0,
'grandTotal' => $grandTotal,
'totalPaid' => $newPaid, 'totalPaid' => $newPaid,
'gstAmount' => $invoice->gst_amount, 'remaining' => $remaining,
'finalAmountWithGst' => $invoice->final_amount_with_gst, 'newStatus' => $newStatus,
'baseAmount' => $invoice->final_amount, 'isCompleted' => $remaining <= 0,
'remaining' => max(0, $invoice->final_amount_with_gst - $newPaid), 'isZero' => $newPaid == 0,
'isCompleted' => $newPaid >= $invoice->final_amount_with_gst
]); ]);
} }
// -------------------------------------------------------------
// INSTALLMENTS (DELETE)
// -------------------------------------------------------------
public function deleteInstallment($id) public function deleteInstallment($id)
{ {
$installment = InvoiceInstallment::findOrFail($id); $installment = InvoiceInstallment::findOrFail($id);
$invoice = $installment->invoice; $invoice = $installment->invoice;
$installment->delete(); $installment->delete();
$invoice->refresh();
$grandTotal = $invoice->grand_total_with_charges ?? 0;
$paidTotal = $invoice->installments()->sum('amount'); $paidTotal = $invoice->installments()->sum('amount');
$remaining = $invoice->final_amount_with_gst - $paidTotal; $remaining = max(0, $grandTotal - $paidTotal);
// Update status if not fully paid anymore $isOverdue = now()->startOfDay()->gt(\Carbon\Carbon::parse($invoice->due_date)->startOfDay());
if ($remaining > 0 && $invoice->status === "paid") {
$invoice->update(['status' => 'pending']);
$this->generateInvoicePDF($invoice); 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.',
'chargeGroupsTotal' => $invoice->charge_groups_total ?? 0,
'gstAmount' => $invoice->gst_amount ?? 0,
'grandTotal' => $grandTotal,
'totalPaid' => $paidTotal, 'totalPaid' => $paidTotal,
'gstAmount' => $invoice->gst_amount,
'finalAmountWithGst' => $invoice->final_amount_with_gst,
'baseAmount' => $invoice->final_amount,
'remaining' => $remaining, 'remaining' => $remaining,
'isZero' => $paidTotal == 0 'newStatus' => $newStatus,
'isZero' => $paidTotal == 0,
]); ]);
} }
// -------------------------------------------------------------
// CHARGE GROUP SAVE
// -------------------------------------------------------------
public function storeChargeGroup(Request $request, $invoiceId)
{
Log::info('🟡 storeChargeGroup HIT', [
'invoice_id' => $invoiceId,
'payload' => $request->all(),
]);
$invoice = Invoice::with('items', 'chargeGroups')->findOrFail($invoiceId);
$data = $request->validate([
'groupname' => 'required|string|max:255',
'basistype' => 'required|in:ttl_qty,amount,ttl_cbm,ttl_kg',
'basisvalue' => 'required|numeric',
'rate' => 'required|numeric|min:0.0001',
'autototal' => 'required|numeric|min:0.01',
'itemids' => 'required|array',
'itemids.*' => 'integer|exists:invoice_items,id',
'tax_type' => 'nullable|in:none,gst,igst',
'gst_percent' => 'nullable|numeric|min:0|max:28',
'total_with_gst' => 'nullable|numeric|min:0',
]);
Log::info('✅ storeChargeGroup VALIDATED', $data);
$exists = InvoiceChargeGroup::where('invoice_id', $invoice->id)
->where('group_name', $data['groupname'])
->exists();
if ($exists) {
return back()
->withErrors(['groupname' => 'This group name is already used for this invoice.'])
->withInput();
}
$taxType = $data['tax_type'] ?? 'gst';
$gstPercent = $data['gst_percent'] ?? 0;
$baseTotal = $data['autototal'];
$totalWithGst = $data['total_with_gst'] ?? $baseTotal;
if ($totalWithGst == 0 && $gstPercent > 0) {
$gstAmount = ($baseTotal * $gstPercent) / 100;
$totalWithGst = $baseTotal + $gstAmount;
}
$group = InvoiceChargeGroup::create([
'invoice_id' => $invoice->id,
'group_name' => $data['groupname'],
'basis_type' => $data['basistype'],
'basis_value' => $data['basisvalue'],
'rate' => $data['rate'],
'total_charge' => $baseTotal,
'tax_type' => $taxType,
'gst_percent' => $gstPercent,
'total_with_gst' => $totalWithGst,
]);
foreach ($data['itemids'] as $itemId) {
InvoiceChargeGroupItem::create([
'group_id' => $group->id,
'invoice_item_id' => $itemId,
]);
}
$invoice->load('chargeGroups');
$chargeGroupsBase = $invoice->chargeGroups->sum('total_charge');
$chargeGroupsWithG = $invoice->chargeGroups->sum('total_with_gst');
$chargeGroupsGst = $chargeGroupsWithG - $chargeGroupsBase;
$invoiceGstPercent = $group->gst_percent ?? 0;
$invoiceTaxType = $group->tax_type ?? 'gst';
$cgstPercent = 0;
$sgstPercent = 0;
$igstPercent = 0;
if ($invoiceTaxType === 'gst') {
$cgstPercent = $invoiceGstPercent / 2;
$sgstPercent = $invoiceGstPercent / 2;
} elseif ($invoiceTaxType === 'igst') {
$igstPercent = $invoiceGstPercent;
}
$invoice->update([
'charge_groups_total' => $chargeGroupsBase,
'gst_amount' => $chargeGroupsGst,
'gst_percent' => $invoiceGstPercent,
'tax_type' => $invoiceTaxType,
'cgst_percent' => $cgstPercent,
'sgst_percent' => $sgstPercent,
'igst_percent' => $igstPercent,
'final_amount' => $chargeGroupsBase,
'final_amount_with_gst' => $chargeGroupsWithG,
'grand_total_with_charges' => $chargeGroupsWithG,
]);
Log::info('✅ Invoice updated from Charge Group (CG total + GST)', [
'invoice_id' => $invoice->id,
'charge_groups_total' => $chargeGroupsBase,
'gst_amount' => $chargeGroupsGst,
'grand_total_with_charges'=> $invoice->grand_total_with_charges,
]);
return response()->json([
'success' => true,
'message' => 'Charge group saved successfully.',
'group_id' => $group->id,
]);
}
// ============================================
// 🆕 PDF DOWNLOAD (Direct browser download)
// ============================================
public function downloadPdf($id)
{
$invoice = Invoice::with(['items', 'customer', 'container', 'chargeGroups.items', 'installments'])
->findOrFail($id);
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
$folder = public_path('invoices/');
$filePath = $folder . $fileName;
// जर PDF exist नसेल तर generate कर
if (!file_exists($filePath)) {
$this->generateInvoicePDF($invoice);
}
return response()->download($filePath, $fileName);
}
// ============================================
// 🆕 EXCEL DOWNLOAD (CSV format - simple)
// ============================================
public function share($id)
{
$invoice = Invoice::findOrFail($id);
// इथे तुला जसं share करायचंय तसं logic टाक:
// उदा. public link generate करून redirect कर, किंवा WhatsApp deeplink, इ.
$url = route('admin.invoices.popup', $invoice->id); // example: popup link share
return redirect()->away('https://wa.me/?text=' . urlencode($url));
}
} }

View File

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

View File

@@ -3,54 +3,199 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Order;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
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)
{ {
// ------------------------------- $reports = $this->buildContainerReportQuery($request)->get();
// FETCH REPORT DATA
// ONLY orders that have BOTH:
// 1. Invoice
// 2. Shipment
// -------------------------------
$reports = DB::table('orders')
->join('shipment_items', 'shipment_items.order_id', '=', 'orders.id')
->join('shipments', 'shipments.id', '=', 'shipment_items.shipment_id')
->join('invoices', 'invoices.order_id', '=', 'orders.id')
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'orders.mark_no')
->leftJoin('users', 'users.customer_id', '=', 'mark_list.customer_id')
->select(
'orders.id as order_pk',
'orders.order_id',
'orders.mark_no',
'orders.origin',
'orders.destination',
'shipments.id as shipment_pk',
'shipments.shipment_id',
'shipments.status as shipment_status',
'shipments.shipment_date',
'invoices.invoice_number',
'invoices.invoice_date',
'invoices.final_amount',
'invoices.status as invoice_status',
'mark_list.company_name',
'mark_list.customer_name'
)
->orderBy('shipments.shipment_date', 'desc')
->get();
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

@@ -85,7 +85,6 @@ public function approveProfileUpdate($id)
$req = \App\Models\UpdateRequest::findOrFail($id); $req = \App\Models\UpdateRequest::findOrFail($id);
$user = \App\Models\User::findOrFail($req->user_id); $user = \App\Models\User::findOrFail($req->user_id);
// FIX: Ensure data is array
$newData = is_array($req->data) ? $req->data : json_decode($req->data, true); $newData = is_array($req->data) ? $req->data : json_decode($req->data, true);
foreach ($newData as $key => $value) { foreach ($newData as $key => $value) {
@@ -96,8 +95,18 @@ public function approveProfileUpdate($id)
} }
} }
// Update user table
$user->save(); $user->save();
// Update mark_list table
\App\Models\MarkList::where('customer_id', $user->customer_id)
->update([
'customer_name' => $user->customer_name,
'company_name' => $user->company_name,
'mobile_no' => $user->mobile_no
]);
// Update request status
$req->status = 'approved'; $req->status = 'approved';
$req->admin_note = 'Approved by admin on ' . now(); $req->admin_note = 'Approved by admin on ' . now();
$req->save(); $req->save();

View File

@@ -0,0 +1,964 @@
<?php
namespace App\Http\Controllers;
use App\Models\Container;
use App\Models\ContainerRow;
use App\Models\MarkList;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use Illuminate\Http\Request;
use Maatwebsite\Excel\Facades\Excel;
use Carbon\Carbon;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\Storage;
class ContainerController extends Controller
{
public function index()
{
$containers = Container::with('rows')->latest()->get();
$containers->each(function ($container) {
$rows = $container->rows;
$totalCtn = 0;
$totalQty = 0;
$totalCbm = 0;
$totalKg = 0;
$ctnKeys = ['CTN', 'CTNS'];
$qtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY', 'QTY', 'PCS', 'PIECES'];
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
$getFirstNumeric = function (array $data, array $possibleKeys) {
$normalizedMap = [];
foreach ($data as $key => $value) {
if ($key === null || $key === '') continue;
$normKey = strtoupper((string)$key);
$normKey = str_replace([' ', '/', '-', '.'], '', $normKey);
$normalizedMap[$normKey] = $value;
}
foreach ($possibleKeys as $search) {
$normSearch = strtoupper($search);
$normSearch = str_replace([' ', '/', '-', '.'], '', $normSearch);
foreach ($normalizedMap as $nKey => $value) {
if (
strpos($nKey, $normSearch) !== false &&
(is_numeric($value) || (is_string($value) && is_numeric(trim($value))))
) {
return (float) trim($value);
}
}
}
return 0;
};
foreach ($rows as $row) {
$data = $row->data ?? [];
$totalCtn += $getFirstNumeric($data, $ctnKeys);
$totalQty += $getFirstNumeric($data, $qtyKeys);
$totalCbm += $getFirstNumeric($data, $cbmKeys);
$totalKg += $getFirstNumeric($data, $kgKeys);
}
$container->summary = [
'total_ctn' => round($totalCtn, 2),
'total_qty' => round($totalQty, 2),
'total_cbm' => round($totalCbm, 3),
'total_kg' => round($totalKg, 2),
];
});
return view('admin.container', compact('containers'));
}
public function create()
{
return view('admin.container_create');
}
private function isValidExcelFormat($rows, $header)
{
if (empty($header) || count($rows) < 2) return false;
$validKeywords = [
'MARK', 'DESCRIPTION', 'DESC', 'CTN', 'CTNS', 'QTY', 'TOTALQTY', 'ITLQTY', 'ITL QTY',
'UNIT', 'CBM', 'TOTAL CBM', 'KG', 'TOTAL KG',
'METAL BUCKLE', 'WATCH MOVEMENT', 'STEEL BOTTLE',
'MEHULPAID', 'ITEM NO', 'ITEM NO.', 'SAHILPAID', 'PINAKIN', 'GST',
'MOON LAMP', 'TRANSPARENT BOTTLE', 'PLASTIC FONDANT',
];
$headerText = implode(' ', array_map('strtoupper', $header));
$requiredHeaders = ['CTN', 'QTY', 'DESCRIPTION', 'DESC'];
$hasValidHeaders = false;
foreach ($requiredHeaders as $key) {
if (stripos($headerText, $key) !== false) {
$hasValidHeaders = true;
break;
}
}
if (!$hasValidHeaders) return false;
$dataPreview = '';
for ($i = 0; $i < min(5, count($rows)); $i++) {
$rowText = implode(' ', array_slice($rows[$i], 0, 10));
$dataPreview .= ' ' . strtoupper((string)$rowText);
}
$validMatches = 0;
foreach ($validKeywords as $keyword) {
if (stripos($headerText . $dataPreview, strtoupper($keyword)) !== false) {
$validMatches++;
}
}
return $validMatches >= 3;
}
private function normalizeKey($value): string
{
$norm = strtoupper((string)$value);
return str_replace([' ', '/', '-', '.'], '', $norm);
}
public function store(Request $request)
{
$request->validate([
'container_name' => 'required|string',
'container_number' => 'required|string|unique:containers,container_number',
'container_date' => 'required|date',
'excel_file' => 'required|file|mimes:xls,xlsx',
]);
$file = $request->file('excel_file');
$sheets = Excel::toArray([], $file);
$rows = $sheets[0] ?? [];
if (count($rows) < 2) {
return back()
->withErrors(['excel_file' => 'Excel file is empty.'])
->withInput();
}
// HEADER DETECTION
$headerRowIndex = null;
$header = [];
foreach ($rows as $i => $row) {
$trimmed = array_map(fn($v) => trim((string)$v), $row);
$nonEmpty = array_filter($trimmed, fn($v) => $v !== '');
if (empty($nonEmpty)) continue;
if (count($nonEmpty) >= 4) {
$headerRowIndex = $i;
$header = $trimmed;
break;
}
}
if ($headerRowIndex === null) {
return back()
->withErrors(['excel_file' => 'Header row not found in Excel.'])
->withInput();
}
if (!$this->isValidExcelFormat($rows, $header)) {
return back()
->withErrors(['excel_file' => 'Only MEHUL / SAHIL / PINAKIN / GST loading list formats allowed.'])
->withInput();
}
// COLUMN INDEXES
$essentialColumns = [
'desc_col' => null,
'ctn_col' => null,
'qty_col' => null,
'totalqty_col' => null,
'unit_col' => null,
'price_col' => null,
'amount_col' => null,
'cbm_col' => null,
'totalcbm_col' => null,
'kg_col' => null,
'totalkg_col' => null,
'itemno_col' => null,
'shopno_col' => null,
];
foreach ($header as $colIndex => $headingText) {
if (empty($headingText)) continue;
$normalized = $this->normalizeKey($headingText);
if (strpos($normalized, 'DESCRIPTION') !== false || strpos($normalized, 'DESC') !== false) {
$essentialColumns['desc_col'] = $colIndex;
} elseif (strpos($normalized, 'CTN') !== false || strpos($normalized, 'CTNS') !== false) {
$essentialColumns['ctn_col'] = $colIndex;
} elseif (
strpos($normalized, 'ITLQTY') !== false ||
strpos($normalized, 'TOTALQTY') !== false ||
strpos($normalized, 'TTLQTY') !== false
) {
$essentialColumns['totalqty_col'] = $colIndex;
} elseif (strpos($normalized, 'QTY') !== false) {
$essentialColumns['qty_col'] = $colIndex;
} elseif (strpos($normalized, 'UNIT') !== false) {
$essentialColumns['unit_col'] = $colIndex;
} elseif (strpos($normalized, 'PRICE') !== false) {
$essentialColumns['price_col'] = $colIndex;
} elseif (strpos($normalized, 'AMOUNT') !== false) {
$essentialColumns['amount_col'] = $colIndex;
} elseif (strpos($normalized, 'TOTALCBM') !== false || strpos($normalized, 'ITLCBM') !== false) {
$essentialColumns['totalcbm_col'] = $colIndex;
} elseif (strpos($normalized, 'CBM') !== false) {
$essentialColumns['cbm_col'] = $colIndex;
} elseif (strpos($normalized, 'TOTALKG') !== false || strpos($normalized, 'TTKG') !== false) {
$essentialColumns['totalkg_col'] = $colIndex;
} elseif (strpos($normalized, 'KG') !== false) {
$essentialColumns['kg_col'] = $colIndex;
} elseif (
strpos($normalized, 'MARKNO') !== false ||
strpos($normalized, 'MARK') !== false ||
strpos($normalized, 'ITEMNO') !== false ||
strpos($normalized, 'ITEM') !== false
) {
$essentialColumns['itemno_col'] = $colIndex;
} elseif (
strpos($normalized, 'SHOPNO') !== false ||
strpos($normalized, 'SHOP') !== false
) {
$essentialColumns['shopno_col'] = $colIndex;
}
}
if (is_null($essentialColumns['itemno_col'])) {
return back()
->withErrors(['excel_file' => 'Mark / Item column not found in Excel (expected headers like MARK NO / Mark_No / Item_No).'])
->withInput();
}
// ROWS CLEANING
$dataRows = array_slice($rows, $headerRowIndex + 1);
$cleanedRows = [];
$unmatchedRowsData = [];
foreach ($dataRows as $offset => $row) {
$trimmedRow = array_map(fn($v) => trim((string)$v), $row);
$nonEmptyCells = array_filter($trimmedRow, fn($v) => $v !== '');
if (count($nonEmptyCells) < 2) continue;
$rowText = strtoupper(implode(' ', $trimmedRow));
if (
stripos($rowText, 'TOTAL') !== false ||
stripos($rowText, 'TTL') !== false ||
stripos($rowText, 'GRAND') !== false
) {
continue;
}
$descValue = '';
if ($essentialColumns['desc_col'] !== null) {
$descValue = trim($row[$essentialColumns['desc_col']] ?? '');
}
if ($essentialColumns['desc_col'] !== null && $descValue === '' && count($nonEmptyCells) >= 1) {
continue;
}
$cleanedRows[] = [
'row' => $row,
'offset' => $offset,
];
}
if (empty($cleanedRows)) {
return back()
->withErrors(['excel_file' => 'No valid item rows found in Excel.'])
->withInput();
}
// FORMULA CHECK
$cleanNumber = function ($value) {
if (is_string($value)) {
$value = str_replace(',', '', trim($value));
}
return is_numeric($value) ? (float)$value : 0;
};
$formulaErrors = [];
foreach ($cleanedRows as $item) {
$row = $item['row'];
$offset = $item['offset'];
$ctn = $essentialColumns['ctn_col'] !== null ? $cleanNumber($row[$essentialColumns['ctn_col']] ?? 0) : 0;
$qty = $essentialColumns['qty_col'] !== null ? $cleanNumber($row[$essentialColumns['qty_col']] ?? 0) : 0;
$ttlQ = $essentialColumns['totalqty_col'] !== null ? $cleanNumber($row[$essentialColumns['totalqty_col']] ?? 0) : 0;
$cbm = $essentialColumns['cbm_col'] !== null ? $cleanNumber($row[$essentialColumns['cbm_col']] ?? 0) : 0;
$ttlC = $essentialColumns['totalcbm_col'] !== null ? $cleanNumber($row[$essentialColumns['totalcbm_col']] ?? 0) : 0;
$kg = $essentialColumns['kg_col'] !== null ? $cleanNumber($row[$essentialColumns['kg_col']] ?? 0) : 0;
$ttlK = $essentialColumns['totalkg_col'] !== null ? $cleanNumber($row[$essentialColumns['totalkg_col']] ?? 0) : 0;
$price = $essentialColumns['price_col'] !== null ? $cleanNumber($row[$essentialColumns['price_col']] ?? 0) : 0;
$ttlAmount = $essentialColumns['amount_col'] !== null ? $cleanNumber($row[$essentialColumns['amount_col']] ?? 0) : 0;
$desc = $essentialColumns['desc_col'] !== null ? (string)($row[$essentialColumns['desc_col']] ?? '') : '';
$mark = $essentialColumns['itemno_col'] !== null ? (string)($row[$essentialColumns['itemno_col']] ?? '') : '';
$expTtlQty = $qty * $ctn;
$expTtlCbm = $cbm * $ctn;
$expTtlKg = $kg * $ctn;
$expTtlAmount = ($qty * $ctn) * $price;
$rowErrors = [];
if (abs($ttlQ - $expTtlQty) > 0.01) {
$rowErrors['TOTAL QTY'] = [
'actual' => $ttlQ,
'expected' => $expTtlQty,
];
}
if (abs($ttlC - $expTtlCbm) > 0.0005) {
$rowErrors['TOTAL CBM'] = [
'actual' => $ttlC,
'expected' => $expTtlCbm,
];
}
if (abs($ttlK - $expTtlKg) > 0.01) {
$rowErrors['TOTAL KG'] = [
'actual' => $ttlK,
'expected' => $expTtlKg,
];
}
if ($essentialColumns['amount_col'] !== null && $essentialColumns['price_col'] !== null) {
if (abs($ttlAmount - $expTtlAmount) > 0.01) {
$rowErrors['TOTAL AMOUNT'] = [
'actual' => $ttlAmount,
'expected' => $expTtlAmount,
];
}
}
if (!empty($rowErrors)) {
$rowData = [];
foreach ($header as $colIndex => $headingText) {
$value = $row[$colIndex] ?? null;
if (is_string($value)) $value = trim($value);
$rowData[$headingText] = $value;
}
$formulaErrors[] = [
'excel_row' => $headerRowIndex + 1 + $offset,
'mark_no' => $mark,
'description' => $desc,
'errors' => $rowErrors,
'data' => $rowData,
];
}
}
// MARK CHECK
$marksFromExcel = [];
foreach ($cleanedRows as $item) {
$row = $item['row'];
$rawMark = $row[$essentialColumns['itemno_col']] ?? null;
$mark = trim((string)($rawMark ?? ''));
if ($mark !== '') {
$marksFromExcel[] = $mark;
}
}
$marksFromExcel = array_values(array_unique($marksFromExcel));
if (empty($marksFromExcel)) {
return back()
->withErrors(['excel_file' => 'No mark numbers found in Excel file.'])
->withInput();
}
$validMarks = MarkList::whereIn('mark_no', $marksFromExcel)
->where('status', 'active')
->pluck('mark_no')
->toArray();
$unmatchedMarks = array_values(array_diff($marksFromExcel, $validMarks));
$markErrors = [];
if (!empty($unmatchedMarks)) {
foreach ($cleanedRows as $item) {
$row = $item['row'];
$offset = $item['offset'];
$rowMark = trim((string)($row[$essentialColumns['itemno_col']] ?? ''));
if ($rowMark === '' || !in_array($rowMark, $unmatchedMarks)) {
continue;
}
$rowData = [];
foreach ($header as $colIndex => $headingText) {
$value = $row[$colIndex] ?? null;
if (is_string($value)) $value = trim($value);
$rowData[$headingText] = $value;
}
$markErrors[] = [
'excel_row' => $headerRowIndex + 1 + $offset,
'mark_no' => $rowMark,
'data' => $rowData,
];
}
}
if (!empty($formulaErrors) || !empty($markErrors)) {
return back()
->withInput()
->with([
'formula_errors' => $formulaErrors,
'mark_errors' => $markErrors,
]);
}
// STEP 1: Marks → customers mapping + grouping
$markRecords = MarkList::whereIn('mark_no', $marksFromExcel)
->where('status', 'active')
->get();
$markToCustomerId = [];
$markToSnapshot = [];
foreach ($markRecords as $mr) {
$markToCustomerId[$mr->mark_no] = $mr->customer_id;
$markToSnapshot[$mr->mark_no] = [
'customer_name' => $mr->customer_name,
'company_name' => $mr->company_name,
'mobile_no' => $mr->mobile_no,
];
}
$groupedByCustomer = [];
foreach ($cleanedRows as $item) {
$row = $item['row'];
$offset = $item['offset'];
$rawMark = $row[$essentialColumns['itemno_col']] ?? null;
$mark = trim((string)($rawMark ?? ''));
if ($mark === '') {
continue;
}
$customerId = $markToCustomerId[$mark] ?? null;
if (!$customerId) {
continue;
}
if (!isset($groupedByCustomer[$customerId])) {
$groupedByCustomer[$customerId] = [];
}
$groupedByCustomer[$customerId][] = [
'row' => $row,
'offset' => $offset,
'mark' => $mark,
];
}
// STEP 2: Container + ContainerRows save
$container = Container::create([
'container_name' => $request->container_name,
'container_number' => $request->container_number,
'container_date' => $request->container_date,
'status' => 'pending',
]);
$path = $file->store('containers');
$container->update(['excel_file' => $path]);
$savedCount = 0;
foreach ($cleanedRows as $item) {
$row = $item['row'];
$offset = $item['offset'];
$data = [];
foreach ($header as $colIndex => $headingText) {
$value = $row[$colIndex] ?? null;
if (is_string($value)) $value = trim($value);
$data[$headingText] = $value;
}
ContainerRow::create([
'container_id' => $container->id,
'row_index' => $headerRowIndex + 1 + $offset,
'data' => $data,
]);
$savedCount++;
}
// STEP 3: per-customer invoices + invoice items
$invoiceCount = 0;
foreach ($groupedByCustomer as $customerId => $rowsForCustomer) {
if (empty($rowsForCustomer)) {
continue;
}
$firstMark = $rowsForCustomer[0]['mark'];
$snap = $markToSnapshot[$firstMark] ?? null;
$customerUser = \App\Models\User::where('customer_id', $customerId)->first();
$invoice = new Invoice();
$invoice->container_id = $container->id;
$invoice->customer_id = $customerUser->id ?? null;
$invoice->mark_no = $firstMark;
$invoice->invoice_number = $this->generateInvoiceNumber();
$invoice->invoice_date = $container->container_date;
$invoice->due_date = Carbon::parse($invoice->invoice_date)
->addDays(10)
->format('Y-m-d');
if ($snap) {
$invoice->customer_name = $snap['customer_name'] ?? null;
$invoice->company_name = $snap['company_name'] ?? null;
$invoice->customer_mobile = $snap['mobile_no'] ?? null;
}
if ($customerUser) {
$invoice->customer_email = $customerUser->email ?? null;
$invoice->customer_address = $customerUser->address ?? null;
$invoice->pincode = $customerUser->pincode ?? null;
}
$invoice->final_amount = 0;
$invoice->gst_percent = 0;
$invoice->gst_amount = 0;
$invoice->final_amount_with_gst = 0;
$uniqueMarks = array_unique(array_column($rowsForCustomer, 'mark'));
$invoice->notes = 'Auto-created from Container ' . $container->container_number
. ' for Mark(s): ' . implode(', ', $uniqueMarks);
$invoice->pdf_path = null;
$invoice->status = 'pending';
$invoice->save();
$invoiceCount++;
foreach ($rowsForCustomer as $item) {
$row = $item['row'];
$offset = $item['offset'];
$mark = $item['mark']; // ✅ mark_no from Excel
$description = $essentialColumns['desc_col'] !== null ? ($row[$essentialColumns['desc_col']] ?? null) : null;
$ctn = $essentialColumns['ctn_col'] !== null ? (int) ($row[$essentialColumns['ctn_col']] ?? 0) : 0;
$qty = $essentialColumns['qty_col'] !== null ? (int) ($row[$essentialColumns['qty_col']] ?? 0) : 0;
$ttlQty = $essentialColumns['totalqty_col'] !== null ? (int) ($row[$essentialColumns['totalqty_col']] ?? 0) : $qty;
$unit = $essentialColumns['unit_col'] !== null ? ($row[$essentialColumns['unit_col']] ?? null) : null;
$price = $essentialColumns['price_col'] !== null ? (float) ($row[$essentialColumns['price_col']] ?? 0) : 0;
$ttlAmount = $essentialColumns['amount_col'] !== null ? (float) ($row[$essentialColumns['amount_col']] ?? 0) : 0;
$cbm = $essentialColumns['cbm_col'] !== null ? (float) ($row[$essentialColumns['cbm_col']] ?? 0) : 0;
$ttlCbm = $essentialColumns['totalcbm_col'] !== null ? (float) ($row[$essentialColumns['totalcbm_col']] ?? $cbm) : $cbm;
$kg = $essentialColumns['kg_col'] !== null ? (float) ($row[$essentialColumns['kg_col']] ?? 0) : 0;
$ttlKg = $essentialColumns['totalkg_col'] !== null ? (float) ($row[$essentialColumns['totalkg_col']] ?? $kg) : $kg;
$shopNo = $essentialColumns['shopno_col'] !== null ? ($row[$essentialColumns['shopno_col']] ?? null) : null;
$rowIndex = $headerRowIndex + 1 + $offset;
InvoiceItem::create([
'invoice_id' => $invoice->id,
'container_id' => $container->id,
'container_row_index' => $rowIndex,
'description' => $description,
'ctn' => $ctn,
'qty' => $qty,
'ttl_qty' => $ttlQty,
'unit' => $unit,
'price' => $price,
'ttl_amount' => $ttlAmount,
'cbm' => $cbm,
'ttl_cbm' => $ttlCbm,
'kg' => $kg,
'ttl_kg' => $ttlKg,
'shop_no' => $shopNo,
'mark_no' => $mark, // ✅ save mark_no from Excel
]);
}
}
$msg = "Container '{$container->container_number}' created with {$savedCount} rows and {$invoiceCount} customer invoice(s).";
return redirect()->route('containers.index')->with('success', $msg);
}
public function show(Container $container)
{
$container->load('rows');
$lockedRowIndexes = \App\Models\Invoice::whereIn('invoices.status', ['paid', 'paying'])
->where('invoices.container_id', $container->id)
->join('invoice_items', 'invoices.id', '=', 'invoice_items.invoice_id')
->pluck('invoice_items.container_row_index')
->filter()
->unique()
->values()
->toArray();
return view('admin.container_show', compact('container', 'lockedRowIndexes'));
}
public function updateRows(Request $request, Container $container)
{
$rowsInput = $request->input('rows', []);
foreach ($rowsInput as $rowId => $cols) {
$row = ContainerRow::where('container_id', $container->id)
->where('id', $rowId)
->first();
if (!$row) {
continue;
}
$data = $row->data ?? [];
foreach ($cols as $colHeader => $value) {
$data[$colHeader] = $value;
}
$row->update(['data' => $data]);
$normalizedMap = [];
foreach ($data as $key => $value) {
if ($key === null || $key === '') {
continue;
}
$normKey = strtoupper((string)$key);
$normKey = str_replace([' ', '/', '-', '.'], '', $normKey);
$normalizedMap[$normKey] = $value;
}
$getFirstNumeric = function (array $map, array $possibleKeys) {
foreach ($possibleKeys as $search) {
$normSearch = strtoupper($search);
$normSearch = str_replace([' ', '/', '-', '.'], '', $normSearch);
foreach ($map as $nKey => $value) {
if (strpos($nKey, $normSearch) !== false) {
if (is_numeric($value)) {
return (float)$value;
}
if (is_string($value) && is_numeric(trim($value))) {
return (float)trim($value);
}
}
}
}
return 0;
};
$ctnKeys = ['CTN', 'CTNS'];
$qtyKeys = ['QTY', 'PCS', 'PIECES'];
$ttlQtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY'];
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
$amountKeys = ['AMOUNT', 'TTLAMOUNT', 'TOTALAMOUNT'];
$ctn = $getFirstNumeric($normalizedMap, $ctnKeys);
$qty = $getFirstNumeric($normalizedMap, $qtyKeys);
$ttlQ = $getFirstNumeric($normalizedMap, $ttlQtyKeys);
if ($ttlQ == 0 && $ctn && $qty) {
$ttlQ = $ctn * $qty;
}
$cbm = $getFirstNumeric($normalizedMap, ['CBM']);
$ttlC = $getFirstNumeric($normalizedMap, ['TOTALCBM', 'TTLCBM', 'ITLCBM']);
if ($ttlC == 0 && $cbm && $ctn) {
$ttlC = $cbm * $ctn;
}
$kg = $getFirstNumeric($normalizedMap, ['KG', 'WEIGHT']);
$ttlK = $getFirstNumeric($normalizedMap, ['TOTALKG', 'TTKG']);
if ($ttlK == 0 && $kg && $ctn) {
$ttlK = $kg * $ctn;
}
$price = $getFirstNumeric($normalizedMap, ['PRICE', 'RATE']);
$amount = $getFirstNumeric($normalizedMap, $amountKeys);
if ($amount == 0 && $price && $ttlQ) {
$amount = $price * $ttlQ;
}
$desc = null;
foreach (['DESCRIPTION', 'DESC'] as $dKey) {
$normD = str_replace([' ', '/', '-', '.'], '', strtoupper($dKey));
foreach ($normalizedMap as $nKey => $v) {
if (strpos($nKey, $normD) !== false) {
$desc = is_string($v) ? trim($v) : $v;
break 2;
}
}
}
$shopNo = null;
foreach (['SHOPNO', 'SHOP'] as $sKey) {
$normS = str_replace([' ', '/', '-', '.'], '', strtoupper($sKey));
foreach ($normalizedMap as $nKey => $v) {
if (strpos($nKey, $normS) !== false) {
$shopNo = is_string($v) ? trim($v) : $v;
break 2;
}
}
}
// ✅ Get mark_no
$markNo = null;
foreach (['MARKNO', 'MARK', 'ITEMNO', 'ITEM'] as $mKey) {
$normM = str_replace([' ', '/', '-', '.'], '', strtoupper($mKey));
foreach ($normalizedMap as $nKey => $v) {
if (strpos($nKey, $normM) !== false) {
$markNo = is_string($v) ? trim($v) : $v;
break 2;
}
}
}
$rowIndex = $row->row_index;
$items = InvoiceItem::where('container_id', $container->id)
->where('container_row_index', $rowIndex)
->get();
if ($items->isEmpty() && $desc) {
$items = InvoiceItem::where('container_id', $container->id)
->whereNull('container_row_index')
->where('description', $desc)
->get();
}
foreach ($items as $item) {
$item->description = $desc;
$item->ctn = $ctn;
$item->qty = $qty;
$item->ttl_qty = $ttlQ;
$item->price = $price;
$item->ttl_amount = $amount;
$item->cbm = $cbm;
$item->ttl_cbm = $ttlC;
$item->kg = $kg;
$item->ttl_kg = $ttlK;
$item->shop_no = $shopNo;
$item->mark_no = $markNo; // ✅ update mark_no
$item->save();
$invoice = $item->invoice;
if ($invoice) {
$newBaseAmount = InvoiceItem::where('invoice_id', $invoice->id)
->sum('ttl_amount');
$taxType = $invoice->tax_type;
$cgstPercent = (float) ($invoice->cgst_percent ?? 0);
$sgstPercent = (float) ($invoice->sgst_percent ?? 0);
$igstPercent = (float) ($invoice->igst_percent ?? 0);
$gstPercent = 0;
if ($taxType === 'gst') {
$gstPercent = $cgstPercent + $sgstPercent;
} elseif ($taxType === 'igst') {
$gstPercent = $igstPercent;
}
$gstAmount = $newBaseAmount * $gstPercent / 100;
$finalWithGst = $newBaseAmount + $gstAmount;
$invoice->final_amount = $newBaseAmount;
$invoice->gst_amount = $gstAmount;
$invoice->final_amount_with_gst = $finalWithGst;
$invoice->gst_percent = $gstPercent;
$invoice->save();
}
}
}
return redirect()
->route('containers.show', $container->id)
->with('success', 'Excel rows updated successfully.');
}
public function updateStatus(Request $request, Container $container)
{
$request->validate([
'status' => 'required|in:container-ready,export-custom,international-transit,arrived-at-india,import-custom,warehouse,domestic-distribution,out-for-delivery,delivered',
]);
$container->status = $request->status;
$container->save();
if ($request->wantsJson() || $request->ajax()) {
return response()->json([
'success' => true,
'status' => $container->status,
]);
}
return back()->with('success', 'Container status updated.');
}
public function destroy(Container $container)
{
$container->delete();
if (request()->wantsJson() || request()->ajax()) {
return response()->json([
'success' => true,
'message' => 'Container deleted',
]);
}
return redirect()
->route('containers.index')
->with('success', 'Container deleted.');
}
private function generateInvoiceNumber(): string
{
$year = now()->format('Y');
$last = Invoice::whereYear('created_at', $year)
->orderBy('id', 'desc')
->first();
if ($last) {
$parts = explode('-', $last->invoice_number);
$seq = 0;
if (count($parts) === 3) {
$seq = (int) $parts[2];
}
$nextSeq = $seq + 1;
} else {
$nextSeq = 1;
}
return 'INV-' . $year . '-' . str_pad($nextSeq, 6, '0', STR_PAD_LEFT);
}
public function downloadPdf(Container $container)
{
$container->load('rows');
$pdf = Pdf::loadView('admin.container_pdf', [
'container' => $container,
])->setPaper('a4', 'landscape');
$fileName = 'container-'.$container->container_number.'.pdf';
return $pdf->download($fileName);
}
public function downloadExcel(Container $container)
{
if (!$container->excel_file) {
abort(404, 'Excel file not found on record.');
}
$path = $container->excel_file;
if (!Storage::exists($path)) {
abort(404, 'Excel file missing on server.');
}
$fileName = 'container-'.$container->container_number.'.xlsx';
return Storage::download($path, $fileName);
}
public function popupPopup(Container $container)
{
$container->load('rows');
$rows = $container->rows ?? collect();
$totalCtn = 0;
$totalQty = 0;
$totalCbm = 0;
$totalKg = 0;
$ctnKeys = ['CTN', 'CTNS'];
$qtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY', 'QTY', 'PCS', 'PIECES'];
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
$getFirstNumeric = function (array $data, array $possibleKeys) {
$normalizedMap = [];
foreach ($data as $key => $value) {
if ($key === null) continue;
$normKey = strtoupper((string)$key);
$normKey = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normKey);
$normalizedMap[$normKey] = $value;
}
foreach ($possibleKeys as $search) {
$normSearch = strtoupper($search);
$normSearch = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normSearch);
foreach ($normalizedMap as $nKey => $value) {
if (strpos($nKey, $normSearch) !== false) {
if (is_numeric($value)) {
return (float)$value;
}
if (is_string($value) && is_numeric(trim($value))) {
return (float)trim($value);
}
}
}
}
return 0;
};
foreach ($rows as $row) {
$data = $row->data ?? [];
if (!is_array($data)) continue;
$totalCtn += $getFirstNumeric($data, $ctnKeys);
$totalQty += $getFirstNumeric($data, $qtyKeys);
$totalCbm += $getFirstNumeric($data, $cbmKeys);
$totalKg += $getFirstNumeric($data, $kgKeys);
}
$summary = [
'total_ctn' => round($totalCtn, 2),
'total_qty' => round($totalQty, 2),
'total_cbm' => round($totalCbm, 3),
'total_kg' => round($totalKg, 2),
];
return view('admin.partials.container_popup_readonly', [
'container' => $container,
'summary' => $summary,
]);
}
}

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();
$inTransit = $containers->whereNotIn('status', [
'delivered',
'warehouse',
'domestic-distribution'
])->count();
$active = $totalOrders; $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,18 +99,26 @@ 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) {
// Extract unique containers
$containers = $invoices->pluck('container')
->filter()
->unique('id')
->values();
$orders = $containers->map(function ($container) {
return [ return [
'order_id' => $o->order_id, 'order_id' => $container->id,
'status' => $o->status, 'container_number' => $container->container_number,
'amount' => $o->ttl_amount, 'status' => $container->status,
'description'=> "Order from {$o->origin} to {$o->destination}", 'container_date' => $container->container_date,
'created_at' => $o->created_at, 'created_at' => $container->created_at,
]; ];
}); });
@@ -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

@@ -20,8 +20,6 @@ class ChatMessage extends Model
'read_by_user', 'read_by_user',
'client_id', 'client_id',
]; ];
/** /**
* The ticket this message belongs to. * The ticket this message belongs to.
*/ */

30
app/Models/Container.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Container extends Model
{
protected $fillable = [
'container_name',
'container_number',
'container_date',
'status',
'excel_file',
];
protected $casts = [
'container_date' => 'date',
];
public function rows()
{
return $this->hasMany(ContainerRow::class);
}
public function invoices()
{
return $this->hasMany(Invoice::class);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ContainerRow extends Model
{
protected $fillable = [
'container_id',
'row_index',
'data',
];
protected $casts = [
'data' => 'array',
];
public function container()
{
return $this->belongsTo(Container::class);
}
}

View File

@@ -10,40 +10,35 @@ class Invoice extends Model
use HasFactory; use HasFactory;
protected $fillable = [ protected $fillable = [
'order_id', 'container_id',
'customer_id', 'customer_id',
'mark_no', 'mark_no',
'invoice_number', 'invoice_number',
'invoice_date', 'invoice_date',
'due_date', 'due_date',
'payment_method', 'payment_method',
'reference_no', 'reference_no',
'status', 'status',
'final_amount',
'final_amount', // without tax 'gst_percent',
'gst_amount',
'tax_type', // gst / igst
'gst_percent', // only used for gst UI input
'cgst_percent',
'sgst_percent',
'igst_percent',
'gst_amount', // total tax amount
'final_amount_with_gst', 'final_amount_with_gst',
'customer_name', 'customer_name',
'company_name', 'company_name',
'customer_email', 'customer_email',
'customer_mobile', 'customer_mobile',
'customer_address', 'customer_address',
'pincode', 'pincode',
'pdf_path', 'pdf_path',
'notes', 'notes',
]; // totals from charge groups
'charge_groups_total',
'grand_total_with_charges',
'tax_type',
'cgst_percent',
'sgst_percent',
'igst_percent',
];
/**************************** /****************************
* Relationships * Relationships
@@ -54,21 +49,31 @@ class Invoice extends Model
return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC'); return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC');
} }
public function order() // public function container()
{ // {
return $this->belongsTo(Order::class); // return $this->belongsTo(Container::class);
} // }
public function customer() public function customer()
{ {
return $this->belongsTo(User::class, 'customer_id'); return $this->belongsTo(User::class, 'customer_id');
} }
public function installments()
{
return $this->hasMany(InvoiceInstallment::class);
}
public function chargeGroups()
{
return $this->hasMany(InvoiceChargeGroup::class, 'invoice_id');
}
/**************************** /****************************
* Helper Functions * Helper Functions
****************************/ ****************************/
// Auto calculate GST fields (you can call this in controller before saving) // (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;
@@ -76,7 +81,6 @@ class Invoice extends Model
$this->final_amount_with_gst = $this->final_amount + $gst; $this->final_amount_with_gst = $this->final_amount + $gst;
} }
// Check overdue status condition
public function isOverdue() public function isOverdue()
{ {
return $this->status === 'pending' && now()->gt($this->due_date); return $this->status === 'pending' && now()->gt($this->due_date);
@@ -84,28 +88,46 @@ class Invoice extends Model
public function getShipment() public function getShipment()
{ {
return $this->order?->shipments?->first(); return null;
} }
public function installments() // ✅ Charge groups base total (WITHOUT GST)
public function getChargeGroupsTotalAttribute()
{
// base = total_charge sum
return (float) $this->chargeGroups->sum('total_charge');
}
// ✅ Grand total: Charge groups base + GST (items ignore)
public function getGrandTotalWithChargesAttribute()
{
$base = (float) ($this->charge_groups_total ?? 0);
$gst = (float) ($this->gst_amount ?? 0);
return $base + $gst;
}
public function totalPaid(): float
{
return (float) $this->installments()->sum('amount');
}
public function remainingAmount(): float
{
$grand = (float) $this->grand_total_with_charges;
$paid = (float) $this->totalPaid();
return max(0, $grand - $paid);
}
public function isLockedForEdit(): bool
{
return $this->status === 'paid';
}
public function container()
{ {
return $this->hasMany(InvoiceInstallment::class); return $this->belongsTo(\App\Models\Container::class, 'container_id');
} }
// App\Models\Invoice.php
public function totalPaid()
{
return $this->installments()->sum('amount');
}
public function remainingAmount()
{
return max(
($this->final_amount_with_gst ?? 0) - $this->totalPaid(),
0
);
}
} }

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class InvoiceChargeGroup extends Model
{
protected $fillable = [
'invoice_id',
'group_name',
'basis_type',
'basis_value',
'rate',
'total_charge',
'tax_type',
'gst_percent',
'total_with_gst',
];
public function invoice()
{
return $this->belongsTo(Invoice::class);
}
public function items()
{
return $this->hasMany(InvoiceChargeGroupItem::class, 'group_id');
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class InvoiceChargeGroupItem extends Model
{
protected $fillable = [
'group_id',
'invoice_item_id',
];
public function group()
{
return $this->belongsTo(InvoiceChargeGroup::class, 'group_id');
}
public function item()
{
return $this->belongsTo(InvoiceItem::class, 'invoice_item_id');
}
}

View File

@@ -11,6 +11,8 @@ class InvoiceItem extends Model
protected $fillable = [ protected $fillable = [
'invoice_id', 'invoice_id',
'container_id', // Container mapping
'container_row_index', // Container row index
'description', 'description',
'ctn', 'ctn',
@@ -27,6 +29,7 @@ class InvoiceItem extends Model
'ttl_kg', 'ttl_kg',
'shop_no', 'shop_no',
'mark_no',
]; ];
/**************************** /****************************
@@ -37,4 +40,79 @@ class InvoiceItem extends Model
{ {
return $this->belongsTo(Invoice::class); return $this->belongsTo(Invoice::class);
} }
public function chargeGroupItems()
{
return $this->hasMany(InvoiceChargeGroupItem::class, 'invoice_item_id');
}
// हे helper: पहिला group fetch करून त्यावरून rate/total काढणे
public function getChargeRateAttribute()
{
$pivot = $this->chargeGroupItems->first();
if (!$pivot || !$pivot->group) {
return 0;
}
$group = $pivot->group;
// basis नुसार या item चा basis value
$basis = 0;
switch ($group->basis_type) {
case 'ttl_qty':
$basis = $this->ttl_qty;
break;
case 'amount':
$basis = $this->ttl_amount;
break;
case 'ttl_cbm':
$basis = $this->ttl_cbm;
break;
case 'ttl_kg':
$basis = $this->ttl_kg;
break;
}
if ($basis <= 0 || ($group->basis_value ?? 0) <= 0) {
return 0;
}
// group चा rate field आधीच आहे, ते direct वापरू
return (float) $group->rate;
}
public function getChargeTotalAttribute()
{
$pivot = $this->chargeGroupItems->first();
if (!$pivot || !$pivot->group) {
return 0;
}
$group = $pivot->group;
$basis = 0;
switch ($group->basis_type) {
case 'ttl_qty':
$basis = $this->ttl_qty;
break;
case 'amount':
$basis = $this->ttl_amount;
break;
case 'ttl_cbm':
$basis = $this->ttl_cbm;
break;
case 'ttl_kg':
$basis = $this->ttl_kg;
break;
}
if ($basis <= 0 || ($group->basis_value ?? 0) <= 0) {
return 0;
}
// per unit rate
$rate = (float) $group->rate;
// item total = basis * rate
return $basis * $rate;
}
} }

View File

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

View File

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

View File

@@ -34,6 +34,9 @@ class CreateInvoiceItemsTable extends Migration
$table->timestamps(); $table->timestamps();
$table->unsignedBigInteger('container_id')->nullable()->after('invoice_id');
$table->integer('container_row_index')->nullable()->after('container_id');
// FK // FK
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade'); $table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');
}); });
@@ -49,4 +52,6 @@ class CreateInvoiceItemsTable extends Migration
}); });
Schema::dropIfExists('invoice_items'); Schema::dropIfExists('invoice_items');
} }
} }

View File

@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('containers', function (Blueprint $table) {
$table->id();
$table->string('container_name');
$table->string('container_number')->unique();
$table->date('container_date');
$table->string('excel_file')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('containers');
}
};

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('loading_list_items', function (Blueprint $table) {
$table->id();
$table->foreignId('container_id')
->constrained('containers')
->onDelete('cascade');
$table->string('mark')->nullable(); // MARK / ITEM NO
$table->string('description')->nullable();
$table->integer('ctn')->nullable();
$table->integer('qty')->nullable();
$table->integer('total_qty')->nullable();
$table->string('unit')->nullable();
$table->decimal('price', 15, 3)->nullable(); // SAHIL format साठी
$table->decimal('cbm', 15, 5)->nullable();
$table->decimal('total_cbm', 15, 5)->nullable();
$table->decimal('kg', 15, 3)->nullable();
$table->decimal('total_kg', 15, 3)->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('loading_list_items');
}
};

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('container_rows', function (Blueprint $table) {
$table->id();
$table->foreignId('container_id')
->constrained('containers')
->onDelete('cascade');
// Excel मधल्या row क्रमांकासाठी (optional)
$table->unsignedInteger('row_index')->nullable();
// या row चा full data: "heading text" => "cell value"
$table->json('data');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('container_rows');
}
};

View File

@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('containers', function (Blueprint $table) {
$table->string('status', 21)
->default('pending')
->after('container_date');
});
}
public function down(): void
{
Schema::table('containers', function (Blueprint $table) {
$table->dropColumn('status');
});
}
};

View File

@@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('invoices', function (Blueprint $table) {
// 1) order_id foreign key काढा
$table->dropForeign(['order_id']);
// 2) order_id column काढा
$table->dropColumn('order_id');
// 3) container_id add करा
$table->unsignedBigInteger('container_id')->nullable()->after('id');
// 4) container_id FK
$table->foreign('container_id')
->references('id')
->on('containers')
->onDelete('cascade');
});
}
public function down(): void
{
Schema::table('invoices', function (Blueprint $table) {
// rollback: container_id काढून order_id परत add
$table->dropForeign(['container_id']);
$table->dropColumn('container_id');
$table->unsignedBigInteger('order_id')->index();
$table->foreign('order_id')
->references('id')
->on('orders')
->onDelete('cascade');
});
}
};

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('invoice_charge_groups', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('invoice_id');
$table->string('group_name')->nullable(); // उदा. "FREIGHT", "HANDLING"
$table->enum('basis_type', ['ttl_qty', 'amount', 'ttl_cbm', 'ttl_kg']);
$table->decimal('basis_value', 15, 3)->default(0); // auto calculate केलेला total basis
$table->decimal('rate', 15, 3)->default(0); // per basis rate (helper)
$table->decimal('total_charge', 15, 2); // admin नी manually टाकलेला total
$table->timestamps();
$table->foreign('invoice_id')
->references('id')->on('invoices')
->onDelete('cascade');
});
}
public function down(): void
{
Schema::dropIfExists('invoice_charge_groups');
}
};

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('invoice_charge_group_items', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('group_id');
$table->unsignedBigInteger('invoice_item_id');
$table->timestamps();
$table->foreign('group_id')
->references('id')->on('invoice_charge_groups')
->onDelete('cascade');
$table->foreign('invoice_item_id')
->references('id')->on('invoice_items')
->onDelete('cascade');
});
}
public function down(): void
{
Schema::dropIfExists('invoice_charge_group_items');
}
};

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::table('invoice_items', function (Blueprint $table) {
$table->unsignedBigInteger('container_id')->nullable()->after('invoice_id');
$table->integer('container_row_index')->nullable()->after('container_id');
});
}
public function down()
{
Schema::table('invoice_items', function (Blueprint $table) {
$table->dropColumn(['container_id', 'container_row_index']);
});
}
};

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('invoices', function (Blueprint $table) {
// column आधीच आहे का हे check करून, नसेल तरच add करायचा
if (!Schema::hasColumn('invoices', 'due_date')) {
$table->date('due_date')
->nullable()
->after('invoice_date');
}
});
}
public function down(): void
{
Schema::table('invoices', function (Blueprint $table) {
if (Schema::hasColumn('invoices', 'due_date')) {
$table->dropColumn('due_date');
}
});
}
};

View File

@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('containers', function (Blueprint $table) {
$table->string('status', 50)->change();
});
}
public function down(): void
{
Schema::table('containers', function (Blueprint $table) {
$table->string('status', 20)->change();
});
}
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,6 +25,12 @@ class PermissionSeeder extends Seeder
// EXTRA (ORDERS) // EXTRA (ORDERS)
'orders.view', // you added this separately 'orders.view', // you added this separately
// CONTAINER
'container.view',
'container.create',
'container.update',
'container.delete',
// SHIPMENT // SHIPMENT
'shipment.view', 'shipment.view',
'shipment.create', 'shipment.create',

BIN
public/images/kentlogo1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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.

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.

Some files were not shown because too many files have changed in this diff Show More