diff --git a/app/Http/Controllers/Admin/AdminInvoiceController.php b/app/Http/Controllers/Admin/AdminInvoiceController.php index 94ac645..62cb0fe 100644 --- a/app/Http/Controllers/Admin/AdminInvoiceController.php +++ b/app/Http/Controllers/Admin/AdminInvoiceController.php @@ -7,6 +7,11 @@ use Illuminate\Http\Request; use App\Models\Invoice; use App\Models\InvoiceItem; use Mpdf\Mpdf; +use App\Models\InvoiceInstallment; +use Illuminate\Support\Facades\Log; + + + class AdminInvoiceController extends Controller { @@ -15,108 +20,242 @@ class AdminInvoiceController extends Controller // ------------------------------------------------------------- public function index() { - $invoices = Invoice::latest()->get(); + $invoices = Invoice::with(['order.shipments'])->latest()->get(); return view('admin.invoice', compact('invoices')); } // ------------------------------------------------------------- // POPUP VIEW (AJAX) // ------------------------------------------------------------- - public function popup($id) + public function popup($id) { - $invoice = Invoice::with('items')->findOrFail($id); - return view('admin.popup_invoice', compact('invoice')); + $invoice = Invoice::with(['items', 'order'])->findOrFail($id); + + // Find actual Shipment record + $shipment = \App\Models\Shipment::whereHas('items', function ($q) use ($invoice) { + $q->where('order_id', $invoice->order_id); + }) + ->first(); + + return view('admin.popup_invoice', compact('invoice', 'shipment')); } + // ------------------------------------------------------------- // EDIT INVOICE PAGE // ------------------------------------------------------------- public function edit($id) { - $invoice = Invoice::findOrFail($id); - return view('admin.invoice_edit', compact('invoice')); + $invoice = Invoice::with(['order.shipments'])->findOrFail($id); + + $shipment = $invoice->order?->shipments?->first(); + + return view('admin.invoice_edit', compact('invoice', 'shipment')); } // ------------------------------------------------------------- // UPDATE INVOICE // ------------------------------------------------------------- - public function update(Request $request, $id) - { - $invoice = Invoice::findOrFail($id); - // Validate editable fields - $data = $request->validate([ - 'invoice_date' => 'required|date', - 'due_date' => 'required|date|after_or_equal:invoice_date', - 'payment_method' => 'nullable|string', - 'reference_no' => 'nullable|string|max:255', - 'final_amount' => 'required|numeric|min:0', - 'gst_percent' => 'required|numeric|min:0|max:28', - 'status' => 'required|in:pending,paid,overdue', - 'notes' => 'nullable|string', - ]); +public function update(Request $request, $id) +{ + Log::info("🟡 Invoice Update Request Received", [ + 'invoice_id' => $id, + 'request' => $request->all() + ]); - // Auto-calc - $gst_amount = ($data['final_amount'] * $data['gst_percent']) / 100; - $final_amount_with_gst = $data['final_amount'] + $gst_amount; + $invoice = Invoice::findOrFail($id); - $data['gst_amount'] = $gst_amount; - $data['final_amount_with_gst'] = $final_amount_with_gst; + $data = $request->validate([ + 'invoice_date' => 'required|date', + 'due_date' => 'required|date|after_or_equal:invoice_date', + 'final_amount' => 'required|numeric|min:0', + 'tax_type' => 'required|in:gst,igst', + 'tax_percent' => 'required|numeric|min:0|max:28', + 'status' => 'required|in:pending,paid,overdue', + 'notes' => 'nullable|string', + ]); - // Update DB - $invoice->update($data); + Log::info("✅ Validated Invoice Update Data", $data); - // Generate PDF - $this->generateInvoicePDF($invoice); + $finalAmount = floatval($data['final_amount']); + $taxPercent = floatval($data['tax_percent']); + $taxAmount = 0; - return redirect() - ->route('admin.invoices.index') - ->with('success', 'Invoice updated & PDF generated successfully.'); + 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; + + // ✅ store original % for UI reference + $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); + + Log::info("✅ Invoice Updated Successfully", [ + 'invoice_id' => $invoice->id + ]); + + // regenerate PDF + $this->generateInvoicePDF($invoice); + + return redirect() + ->route('admin.invoices.index') + ->with('success', 'Invoice updated & PDF generated successfully.'); +} + + // ------------------------------------------------------------- // PDF GENERATION USING mPDF // ------------------------------------------------------------- public function generateInvoicePDF($invoice) { - // PDF Name + // Load relationship including shipment + $invoice->load(['items', 'order.shipments']); + + // Fetch shipment + $shipment = $invoice->order?->shipments?->first(); + + // PDF filename $fileName = 'invoice-' . $invoice->invoice_number . '.pdf'; - // Save directly in /public/invoices + // Directory path $folder = public_path('invoices/'); - // Create folder if not exists if (!file_exists($folder)) { mkdir($folder, 0777, true); } - // Full path $filePath = $folder . $fileName; - // Delete old file + // Delete old file if exists if (file_exists($filePath)) { unlink($filePath); } - // Initialize mPDF + // Create mPDF instance $mpdf = new Mpdf([ 'mode' => 'utf-8', 'format' => 'A4', - 'default_font' => 'sans-serif' + 'default_font' => 'sans-serif', ]); // Load HTML view - $html = view('admin.pdf.invoice', compact('invoice'))->render(); + $html = view('admin.pdf.invoice', [ + 'invoice' => $invoice, + 'shipment' => $shipment + ])->render(); - // Generate + // Write HTML to PDF $mpdf->WriteHTML($html); - // Save to public/invoices + // Save PDF $mpdf->Output($filePath, 'F'); - // Save path in DB + // Update DB path $invoice->update([ 'pdf_path' => 'invoices/' . $fileName ]); } + + + +public function storeInstallment(Request $request, $invoice_id) +{ + $request->validate([ + 'installment_date' => 'required|date', + 'payment_method' => 'required|string', + 'reference_no' => 'nullable|string', + 'amount' => 'required|numeric|min:1', + ]); + + $invoice = Invoice::findOrFail($invoice_id); + + $paidTotal = $invoice->installments()->sum('amount'); + $remaining = $invoice->final_amount - $paidTotal; + + if ($request->amount > $remaining) { + return response()->json([ + 'status' => 'error', + 'message' => 'Installment amount exceeds remaining balance.' + ], 422); + } + + $installment = InvoiceInstallment::create([ + 'invoice_id' => $invoice_id, + 'installment_date' => $request->installment_date, + 'payment_method' => $request->payment_method, + 'reference_no' => $request->reference_no, + 'amount' => $request->amount, + ]); + + // Update invoice status to paid if fully cleared + $newPaid = $paidTotal + $request->amount; + + if ($newPaid >= $invoice->final_amount) { + $invoice->update(['status' => 'paid']); + } + + return response()->json([ + 'status' => 'success', + 'message' => 'Installment added successfully.', + 'installment' => $installment, + 'totalPaid' => $newPaid, + 'remaining' => max(0, $invoice->final_amount - $newPaid), + 'isCompleted' => $newPaid >= $invoice->final_amount + ]); +} + +public function deleteInstallment($id) +{ + $installment = InvoiceInstallment::findOrFail($id); + $invoice = $installment->invoice; + + // delete installment + $installment->delete(); + + // recalc totals + $paidTotal = $invoice->installments()->sum('amount'); + $remaining = $invoice->final_amount - $paidTotal; + + // auto update invoice status + if ($remaining > 0 && $invoice->status === "paid") { + $invoice->update(['status' => 'pending']); + } + + return response()->json([ + 'status' => 'success', + 'message' => 'Installment deleted.', + 'totalPaid' => $paidTotal, + 'remaining' => $remaining, + 'isZero' => $paidTotal == 0 + ]); +} + + } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index e8c5739..b59b456 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -9,34 +9,41 @@ class Invoice extends Model { use HasFactory; - protected $fillable = [ - 'order_id', - 'customer_id', - 'mark_no', + protected $fillable = [ + 'order_id', + 'customer_id', + 'mark_no', - 'invoice_number', - 'invoice_date', - 'due_date', + 'invoice_number', + 'invoice_date', + 'due_date', - 'payment_method', - 'reference_no', - 'status', + 'payment_method', + 'reference_no', + 'status', - 'final_amount', - 'gst_percent', - 'gst_amount', - 'final_amount_with_gst', + 'final_amount', // without tax - 'customer_name', - 'company_name', - 'customer_email', - 'customer_mobile', - 'customer_address', - 'pincode', + 'tax_type', // gst / igst + 'gst_percent', // only used for gst UI input + 'cgst_percent', + 'sgst_percent', + 'igst_percent', + + 'gst_amount', // total tax amount + 'final_amount_with_gst', + + 'customer_name', + 'company_name', + 'customer_email', + 'customer_mobile', + 'customer_address', + 'pincode', + + 'pdf_path', + 'notes', +]; - 'pdf_path', - 'notes', - ]; /**************************** * Relationships @@ -74,4 +81,16 @@ class Invoice extends Model { return $this->status === 'pending' && now()->gt($this->due_date); } + + public function getShipment() + { + return $this->order?->shipments?->first(); + } + + public function installments() +{ + return $this->hasMany(InvoiceInstallment::class); +} + + } diff --git a/app/Models/InvoiceInstallment.php b/app/Models/InvoiceInstallment.php new file mode 100644 index 0000000..a62f63e --- /dev/null +++ b/app/Models/InvoiceInstallment.php @@ -0,0 +1,24 @@ +belongsTo(Invoice::class); + } +} diff --git a/app/Models/Order.php b/app/Models/Order.php index 3113fa7..c1ce73c 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -46,4 +46,15 @@ class Order extends Model ->withTimestamps(); } + public function shipmentItems() + { + return $this->hasMany(\App\Models\ShipmentItem::class, 'order_id', 'id'); + } + + public function shipments() + { + return $this->belongsToMany(\App\Models\Shipment::class, 'shipment_items', 'order_id', 'shipment_id'); + } + + } diff --git a/database/migrations/2025_11_24_052927_update_gst_fields_in_invoices_table.php b/database/migrations/2025_11_24_052927_update_gst_fields_in_invoices_table.php new file mode 100644 index 0000000..0645f02 --- /dev/null +++ b/database/migrations/2025_11_24_052927_update_gst_fields_in_invoices_table.php @@ -0,0 +1,53 @@ +enum('tax_type', ['gst', 'igst']) + ->default('gst') + ->after('final_amount'); + + // Old gst_percent becomes optional + $table->decimal('gst_percent', 5, 2) + ->nullable() + ->change(); + + // Split GST % + $table->decimal('cgst_percent', 5, 2) + ->nullable() + ->after('gst_percent'); + + $table->decimal('sgst_percent', 5, 2) + ->nullable() + ->after('cgst_percent'); + + // IGST % + $table->decimal('igst_percent', 5, 2) + ->nullable() + ->after('sgst_percent'); + + // Tax amount recalculation is the same + // gst_amount and final_amount_with_gst already exist + }); + } + + public function down(): void + { + Schema::table('invoices', function (Blueprint $table) { + $table->dropColumn([ + 'tax_type', + 'cgst_percent', + 'sgst_percent', + 'igst_percent', + ]); + }); + } +}; diff --git a/database/migrations/2025_11_24_131305_create_invoice_installments_table.php b/database/migrations/2025_11_24_131305_create_invoice_installments_table.php new file mode 100644 index 0000000..f9e33cc --- /dev/null +++ b/database/migrations/2025_11_24_131305_create_invoice_installments_table.php @@ -0,0 +1,30 @@ +id(); + + $table->unsignedBigInteger('invoice_id'); + $table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade'); + + $table->date('installment_date'); + $table->string('payment_method')->nullable(); // cash, bank, UPI, cheque, etc + $table->string('reference_no')->nullable(); + $table->decimal('amount', 10, 2); + + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('invoice_installments'); + } +}; diff --git a/public/invoices/invoice-INV-2025-000008.pdf b/public/invoices/invoice-INV-2025-000008.pdf index 8c6af73..e6c8df1 100644 Binary files a/public/invoices/invoice-INV-2025-000008.pdf and b/public/invoices/invoice-INV-2025-000008.pdf differ diff --git a/public/invoices/invoice-INV-2025-000009.pdf b/public/invoices/invoice-INV-2025-000009.pdf new file mode 100644 index 0000000..362d2f4 Binary files /dev/null and b/public/invoices/invoice-INV-2025-000009.pdf differ diff --git a/public/invoices/invoice-INV-2025-000010.pdf b/public/invoices/invoice-INV-2025-000010.pdf new file mode 100644 index 0000000..435c32b Binary files /dev/null and b/public/invoices/invoice-INV-2025-000010.pdf differ diff --git a/public/invoices/invoice-INV-2025-000011.pdf b/public/invoices/invoice-INV-2025-000011.pdf new file mode 100644 index 0000000..5c37efa Binary files /dev/null and b/public/invoices/invoice-INV-2025-000011.pdf differ diff --git a/public/invoices/invoice-INV-2025-000012.pdf b/public/invoices/invoice-INV-2025-000012.pdf new file mode 100644 index 0000000..188bf50 Binary files /dev/null and b/public/invoices/invoice-INV-2025-000012.pdf differ diff --git a/public/invoices/invoice-INV-2025-000013.pdf b/public/invoices/invoice-INV-2025-000013.pdf new file mode 100644 index 0000000..350715c Binary files /dev/null and b/public/invoices/invoice-INV-2025-000013.pdf differ diff --git a/public/invoices/invoice-INV-2025-000014.pdf b/public/invoices/invoice-INV-2025-000014.pdf new file mode 100644 index 0000000..a2f5b70 Binary files /dev/null and b/public/invoices/invoice-INV-2025-000014.pdf differ diff --git a/public/invoices/invoice-INV-2025-000022.pdf b/public/invoices/invoice-INV-2025-000022.pdf new file mode 100644 index 0000000..663e06a Binary files /dev/null and b/public/invoices/invoice-INV-2025-000022.pdf differ diff --git a/resources/views/admin/invoice_edit.blade.php b/resources/views/admin/invoice_edit.blade.php index 8f11fc9..0d9f9df 100644 --- a/resources/views/admin/invoice_edit.blade.php +++ b/resources/views/admin/invoice_edit.blade.php @@ -3,430 +3,369 @@ @section('page-title', 'Edit Invoice') @section('content') + -
+ + +{{-- Invoice Preview Section --}} +
+

+ Invoice Details +

+
+
+ + @include('admin.popup_invoice', [ + 'invoice' => $invoice, + 'shipment' => $shipment, + 'embedded' => true + ]) + +
+
+ + +
+

Edit Invoice

+ @csrf + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+ tax_type === 'gst')> + +
+ +
+ tax_type === 'igst')> + +
+
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+
+ + +
+
+ +@php + $totalPaid = $invoice->installments->sum('amount'); + $remaining = $invoice->final_amount_with_gst - $totalPaid; +@endphp + + +
+ +
+

Installment Payments

+ + @if($remaining > 0) + + @endif +
+ + +
+
+

Add Installment

+
+ +
+
@csrf
- - -
- -
- - + +
- -
- -
- - -
- -
- - -
- -
- - -
- -
- - + + + +
-
- - +
+ +
-
-
@@ -435,47 +374,229 @@ html {
+
+
+ +
+ Total Amount (Before Tax): + ₹{{ number_format($invoice->final_amount, 2) }} +
+ +
+ Tax Type: + + @if($invoice->tax_type === 'gst') + GST (CGST + SGST) + @else + IGST + @endif + +
+ +
+ Tax Percentage: + + @if($invoice->tax_type === 'gst') + {{ $invoice->cgst_percent + $invoice->sgst_percent }}% + @else + {{ $invoice->igst_percent }}% + @endif + +
+ +
+ GST Amount: + ₹{{ number_format($invoice->gst_amount, 2) }} +
+ +
+ +
+ Total Invoice Amount (Including GST): + ₹{{ number_format($invoice->final_amount_with_gst, 2) }} +
+ +
+ +
+ Total Paid: + ₹{{ number_format($totalPaid, 2) }} +
+ +
+ Remaining: + ₹{{ number_format($remaining, 2) }} +
+ +
+
+ + + + +
+
+

Installment History

+
+ +
+ + + + + + + + + + + + + @foreach($invoice->installments as $i) + + + + + + + + + @endforeach + +
#DatePayment MethodReference NoAmountAction
{{ $loop->iteration }}{{ $i->installment_date }}{{ ucfirst($i->payment_method) }}{{ $i->reference_no }}₹{{ number_format($i->amount, 2) }} + +
+
+
+ + -@endsection \ No newline at end of file + + + +@endsection diff --git a/resources/views/admin/layouts/app.blade.php b/resources/views/admin/layouts/app.blade.php index 6ffd561..0980c92 100644 --- a/resources/views/admin/layouts/app.blade.php +++ b/resources/views/admin/layouts/app.blade.php @@ -164,7 +164,7 @@ } .content-wrapper { - padding: 18px 16px 0 16px; + padding: 18px 16px 16px 16px; flex-grow: 1; overflow-y: auto; } diff --git a/resources/views/admin/popup_invoice.blade.php b/resources/views/admin/popup_invoice.blade.php index 4441a79..d8c05d2 100644 --- a/resources/views/admin/popup_invoice.blade.php +++ b/resources/views/admin/popup_invoice.blade.php @@ -1,207 +1,280 @@ -
- -
-
-

- INVOICE -

-

{{ $invoice->invoice_number }}

-
-
-
- - - {{ ucfirst($invoice->status) }} - -
-
-
+{{-- INVOICE CONTENT (NO POPUP WRAPPERS HERE) --}} - -
-
-
-
-
-
-
-
INVOICE DATE
-
-
- {{ \Carbon\Carbon::parse($invoice->invoice_date)->format('M d, Y') }} -
-
-
-
- -
-
-
-
-
DUE DATE
-
-
- {{ \Carbon\Carbon::parse($invoice->due_date)->format('M d, Y') }} -
-
-
-
-
-
-
+ +
- -
-
-
-
-
- Customer Details -
-
-
-
-
-
{{ $invoice->customer_name }}
- @if($invoice->company_name) -

- Company: {{ $invoice->company_name }} -

- @endif -

- Mobile: {{ $invoice->customer_mobile }} -

-

- Email: {{ $invoice->customer_email }} -

-
-
-

- Address:
- {{ $invoice->customer_address }} -

-
-
-
-
-
-
- - -
-
-
-
-
- Invoice Items -
-
-
-
- - - - - - - - - - - - - - - - - - - - @foreach($invoice->items as $i => $item) - - - - - - - - - - - - - - - - @endforeach - -
#DescriptionCTNQTYTTL/QTYUnitPriceTTL AmountCBMTTL CBMKGTTL KGShop No
{{ $i+1 }}{{ $item->description }}{{ $item->ctn }}{{ $item->qty }}{{ $item->ttl_qty }}{{ $item->unit }}₹{{ number_format($item->price,2) }}₹{{ number_format($item->ttl_amount,2) }}{{ $item->cbm }}{{ $item->ttl_cbm }}{{ $item->kg }}{{ $item->ttl_kg }} - {{ $item->shop_no }} -
-
-
-
-
-
- -
-
-
-
-
- Final Summary -
-
-
-
- Amount: - ₹{{ number_format($invoice->final_amount,2) }} +
+ +

+ INVOICE +

+ +

{{ $invoice->invoice_number }}

+ + {{-- ORDER + SHIPMENT INFO --}} +
+ + {{-- ORDER ID --}} + @if($invoice->order_id) +
+ Order ID: + {{ $invoice->order->order_id ?? $invoice->order_id }}
-
- GST ({{ $invoice->gst_percent }}%): - ₹{{ number_format($invoice->gst_amount,2) }} -
-
- Total With GST: - ₹{{ number_format($invoice->final_amount_with_gst,2) }} + @endif + + {{-- SHIPMENT ID --}} + @if(isset($shipment) && $shipment) +
+ Shipment ID: {{ $shipment->shipment_id }}
+ @endif + +
+ +
+ +
+ + + + + + {{ ucfirst($invoice->status) }} + + +
+
+ +
+ + + +
+
+ +
+ +
+ INVOICE DATE +
+ {{ \Carbon\Carbon::parse($invoice->invoice_date)->format('M d, Y') }}
+ +
+ +
+ +
+ DUE DATE +
+ {{ \Carbon\Carbon::parse($invoice->due_date)->format('M d, Y') }} +
+
+
+
- \ No newline at end of file + diff --git a/routes/web.php b/routes/web.php index 8681d0d..f18cd51 100644 --- a/routes/web.php +++ b/routes/web.php @@ -143,6 +143,18 @@ Route::prefix('admin') Route::post('/invoices/{id}/update', [AdminInvoiceController::class, 'update']) ->name('admin.invoices.update'); + Route::post('/invoices/{invoice}/installments', [AdminInvoiceController::class, 'storeInstallment']) + ->name('admin.invoice.installment.store'); + + Route::post('/invoices/{id}/installment', [AdminInvoiceController::class, 'storeInstallment']) + ->name('admin.invoice.installment.store'); + + Route::delete('/installment/{id}', [AdminInvoiceController::class, 'deleteInstallment']) + ->name('admin.invoice.installment.delete'); + + + + //Add New Invoice Route::get('/admin/invoices/create', [InvoiceController::class, 'create'])->name('admin.invoices.create');