This commit is contained in:
Utkarsh Khedkar
2025-11-17 10:36:00 +05:30
16 changed files with 1330 additions and 139 deletions

View File

@@ -0,0 +1,122 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use Mpdf\Mpdf;
class AdminInvoiceController extends Controller
{
// -------------------------------------------------------------
// INVOICE LIST PAGE
// -------------------------------------------------------------
public function index()
{
$invoices = Invoice::latest()->get();
return view('admin.invoice', compact('invoices'));
}
// -------------------------------------------------------------
// POPUP VIEW (AJAX)
// -------------------------------------------------------------
public function popup($id)
{
$invoice = Invoice::with('items')->findOrFail($id);
return view('admin.popup_invoice', compact('invoice'));
}
// -------------------------------------------------------------
// EDIT INVOICE PAGE
// -------------------------------------------------------------
public function edit($id)
{
$invoice = Invoice::findOrFail($id);
return view('admin.invoice_edit', compact('invoice'));
}
// -------------------------------------------------------------
// 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',
]);
// Auto-calc
$gst_amount = ($data['final_amount'] * $data['gst_percent']) / 100;
$final_amount_with_gst = $data['final_amount'] + $gst_amount;
$data['gst_amount'] = $gst_amount;
$data['final_amount_with_gst'] = $final_amount_with_gst;
// Update DB
$invoice->update($data);
// Generate 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
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
// Save directly in /public/invoices
$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
if (file_exists($filePath)) {
unlink($filePath);
}
// Initialize mPDF
$mpdf = new Mpdf([
'mode' => 'utf-8',
'format' => 'A4',
'default_font' => 'sans-serif'
]);
// Load HTML view
$html = view('admin.pdf.invoice', compact('invoice'))->render();
// Generate
$mpdf->WriteHTML($html);
// Save to public/invoices
$mpdf->Output($filePath, 'F');
// Save path in DB
$invoice->update([
'pdf_path' => 'invoices/' . $fileName
]);
}
}

View File

@@ -99,82 +99,163 @@ class AdminOrderController extends Controller
// -------------------------------------------------------------------------
public function finishOrder(Request $request)
{
$request->validate([
'mark_no' => 'required',
'origin' => 'required',
'destination' => 'required',
]);
{
$request->validate([
'mark_no' => 'required',
'origin' => 'required',
'destination' => 'required',
]);
$items = session('temp_order_items', []);
$items = session('temp_order_items', []);
if (empty($items)) {
return redirect()->to(route('admin.orders.index') . '#createOrderForm')
->with('error', 'Add at least one item before finishing.');
}
// Generate Order ID
$year = date('y');
$prefix = "KNT-$year-";
$lastOrder = Order::latest('id')->first();
$nextNumber = $lastOrder ? intval(substr($lastOrder->order_id, -8)) + 1 : 1;
$orderId = $prefix . str_pad($nextNumber, 8, '0', STR_PAD_LEFT);
// TOTAL SUMS
$total_ctn = array_sum(array_column($items, 'ctn'));
$total_qty = array_sum(array_column($items, 'qty'));
$total_ttl_qty = array_sum(array_column($items, 'ttl_qty'));
$total_amount = array_sum(array_column($items, 'ttl_amount'));
$total_cbm = array_sum(array_column($items, 'cbm'));
$total_ttl_cbm = array_sum(array_column($items, 'ttl_cbm'));
$total_kg = array_sum(array_column($items, 'kg'));
$total_ttl_kg = array_sum(array_column($items, 'ttl_kg'));
// CREATE ORDER
$order = Order::create([
'order_id' => $orderId,
'mark_no' => $request->mark_no,
'origin' => $request->origin,
'destination' => $request->destination,
'ctn' => $total_ctn,
'qty' => $total_qty,
'ttl_qty' => $total_ttl_qty,
'ttl_amount' => $total_amount,
'cbm' => $total_cbm,
'ttl_cbm' => $total_ttl_cbm,
'kg' => $total_kg,
'ttl_kg' => $total_ttl_kg,
'status' => 'pending',
]);
// SAVE ALL SUB-ITEMS
foreach ($items as $item) {
OrderItem::create([
'order_id' => $order->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'],
]);
}
// CLEAR TEMP DATA
session()->forget(['temp_order_items', 'mark_no', 'origin', 'destination']);
return redirect()->route('admin.orders.index')
->with('success', 'Order saved successfully.');
if (empty($items)) {
return redirect()->to(route('admin.orders.index') . '#createOrderForm')
->with('error', 'Add at least one item before finishing.');
}
// =======================
// GENERATE ORDER ID
// =======================
$year = date('y');
$prefix = "KNT-$year-";
$lastOrder = Order::latest('id')->first();
$nextNumber = $lastOrder ? intval(substr($lastOrder->order_id, -8)) + 1 : 1;
$orderId = $prefix . str_pad($nextNumber, 8, '0', STR_PAD_LEFT);
// =======================
// TOTAL SUMS
// =======================
$total_ctn = array_sum(array_column($items, 'ctn'));
$total_qty = array_sum(array_column($items, 'qty'));
$total_ttl_qty = array_sum(array_column($items, 'ttl_qty'));
$total_amount = array_sum(array_column($items, 'ttl_amount'));
$total_cbm = array_sum(array_column($items, 'cbm'));
$total_ttl_cbm = array_sum(array_column($items, 'ttl_cbm'));
$total_kg = array_sum(array_column($items, 'kg'));
$total_ttl_kg = array_sum(array_column($items, 'ttl_kg'));
// =======================
// CREATE ORDER
// =======================
$order = Order::create([
'order_id' => $orderId,
'mark_no' => $request->mark_no,
'origin' => $request->origin,
'destination' => $request->destination,
'ctn' => $total_ctn,
'qty' => $total_qty,
'ttl_qty' => $total_ttl_qty,
'ttl_amount' => $total_amount,
'cbm' => $total_cbm,
'ttl_cbm' => $total_ttl_cbm,
'kg' => $total_kg,
'ttl_kg' => $total_ttl_kg,
'status' => 'pending',
]);
// SAVE ORDER ITEMS
foreach ($items as $item) {
OrderItem::create([
'order_id' => $order->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'],
]);
}
// =======================
// INVOICE CREATION START
// =======================
// 1. Auto-generate invoice number
$lastInvoice = \App\Models\Invoice::latest()->first();
$nextInvoice = $lastInvoice ? $lastInvoice->id + 1 : 1;
$invoiceNumber = 'INV-' . date('Y') . '-' . str_pad($nextInvoice, 6, '0', STR_PAD_LEFT);
// 2. Fetch customer (using mark list → customer_id)
$markList = MarkList::where('mark_no', $order->mark_no)->first();
$customer = null;
if ($markList && $markList->customer_id) {
$customer = \App\Models\User::where('customer_id', $markList->customer_id)->first();
}
// 3. Create Invoice Record
$invoice = \App\Models\Invoice::create([
'order_id' => $order->id,
'customer_id' => $customer->id ?? null,
'mark_no' => $order->mark_no,
'invoice_number' => $invoiceNumber,
'invoice_date' => now(),
'due_date' => now()->addDays(10),
'payment_method' => null,
'reference_no' => null,
'status' => 'pending',
'final_amount' => $total_amount,
'gst_percent' => 0,
'gst_amount' => 0,
'final_amount_with_gst' => $total_amount,
// snapshot customer fields
'customer_name' => $customer->customer_name ?? null,
'company_name' => $customer->company_name ?? null,
'customer_email' => $customer->email ?? null,
'customer_mobile' => $customer->mobile_no ?? null,
'customer_address' => $customer->address ?? null,
'pincode' => $customer->pincode ?? null,
'notes' => null,
]);
// 4. Clone order items into invoice_items
foreach ($order->items as $item) {
\App\Models\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,
]);
}
// 5. TODO: PDF generation (I will add this later)
$invoice->pdf_path = null; // placeholder for now
$invoice->save();
// =======================
// END INVOICE CREATION
// =======================
// CLEAR TEMP DATA
session()->forget(['temp_order_items', 'mark_no', 'origin', 'destination']);
return redirect()->route('admin.orders.index')
->with('success', 'Order + Invoice created successfully.');
}
// -------------------------------------------------------------------------
// ORDER SHOW PAGE
// -------------------------------------------------------------------------

77
app/Models/Invoice.php Normal file
View File

@@ -0,0 +1,77 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Invoice extends Model
{
use HasFactory;
protected $fillable = [
'order_id',
'customer_id',
'mark_no',
'invoice_number',
'invoice_date',
'due_date',
'payment_method',
'reference_no',
'status',
'final_amount',
'gst_percent',
'gst_amount',
'final_amount_with_gst',
'customer_name',
'company_name',
'customer_email',
'customer_mobile',
'customer_address',
'pincode',
'pdf_path',
'notes',
];
/****************************
* Relationships
****************************/
public function items()
{
return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC');
}
public function order()
{
return $this->belongsTo(Order::class);
}
public function customer()
{
return $this->belongsTo(User::class, 'customer_id');
}
/****************************
* Helper Functions
****************************/
// Auto calculate GST fields (you can call this in controller before saving)
public function calculateTotals()
{
$gst = ($this->final_amount * $this->gst_percent) / 100;
$this->gst_amount = $gst;
$this->final_amount_with_gst = $this->final_amount + $gst;
}
// Check overdue status condition
public function isOverdue()
{
return $this->status === 'pending' && now()->gt($this->due_date);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class InvoiceItem extends Model
{
use HasFactory;
protected $fillable = [
'invoice_id',
'description',
'ctn',
'qty',
'ttl_qty',
'unit',
'price',
'ttl_amount',
'cbm',
'ttl_cbm',
'kg',
'ttl_kg',
'shop_no',
];
/****************************
* Relationships
****************************/
public function invoice()
{
return $this->belongsTo(Invoice::class);
}
}

View File

@@ -9,6 +9,7 @@
"php": "^8.2",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"mpdf/mpdf": "^8.2",
"php-open-source-saver/jwt-auth": "2.8"
},
"require-dev": {

417
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "dc4cd5d2076c08406e2d0bd4db2cd741",
"content-hash": "fa680e1e8b3550d710849bbd820e3626",
"packages": [
{
"name": "brick/math",
@@ -2184,6 +2184,239 @@
],
"time": "2025-03-24T10:02:05+00:00"
},
{
"name": "mpdf/mpdf",
"version": "v8.2.6",
"source": {
"type": "git",
"url": "https://github.com/mpdf/mpdf.git",
"reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mpdf/mpdf/zipball/dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44",
"reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44",
"shasum": ""
},
"require": {
"ext-gd": "*",
"ext-mbstring": "*",
"mpdf/psr-http-message-shim": "^1.0 || ^2.0",
"mpdf/psr-log-aware-trait": "^2.0 || ^3.0",
"myclabs/deep-copy": "^1.7",
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
"php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"psr/http-message": "^1.0 || ^2.0",
"psr/log": "^1.0 || ^2.0 || ^3.0",
"setasign/fpdi": "^2.1"
},
"require-dev": {
"mockery/mockery": "^1.3.0",
"mpdf/qrcode": "^1.1.0",
"squizlabs/php_codesniffer": "^3.5.0",
"tracy/tracy": "~2.5",
"yoast/phpunit-polyfills": "^1.0"
},
"suggest": {
"ext-bcmath": "Needed for generation of some types of barcodes",
"ext-xml": "Needed mainly for SVG manipulation",
"ext-zlib": "Needed for compression of embedded resources, such as fonts"
},
"type": "library",
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"Mpdf\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-only"
],
"authors": [
{
"name": "Matěj Humpál",
"role": "Developer, maintainer"
},
{
"name": "Ian Back",
"role": "Developer (retired)"
}
],
"description": "PHP library generating PDF files from UTF-8 encoded HTML",
"homepage": "https://mpdf.github.io",
"keywords": [
"pdf",
"php",
"utf-8"
],
"support": {
"docs": "https://mpdf.github.io",
"issues": "https://github.com/mpdf/mpdf/issues",
"source": "https://github.com/mpdf/mpdf"
},
"funding": [
{
"url": "https://www.paypal.me/mpdf",
"type": "custom"
}
],
"time": "2025-08-18T08:51:51+00:00"
},
{
"name": "mpdf/psr-http-message-shim",
"version": "v2.0.1",
"source": {
"type": "git",
"url": "https://github.com/mpdf/psr-http-message-shim.git",
"reference": "f25a0153d645e234f9db42e5433b16d9b113920f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/f25a0153d645e234f9db42e5433b16d9b113920f",
"reference": "f25a0153d645e234f9db42e5433b16d9b113920f",
"shasum": ""
},
"require": {
"psr/http-message": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Mpdf\\PsrHttpMessageShim\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Dorison",
"email": "mark@chromatichq.com"
},
{
"name": "Kristofer Widholm",
"email": "kristofer@chromatichq.com"
},
{
"name": "Nigel Cunningham",
"email": "nigel.cunningham@technocrat.com.au"
}
],
"description": "Shim to allow support of different psr/message versions.",
"support": {
"issues": "https://github.com/mpdf/psr-http-message-shim/issues",
"source": "https://github.com/mpdf/psr-http-message-shim/tree/v2.0.1"
},
"time": "2023-10-02T14:34:03+00:00"
},
{
"name": "mpdf/psr-log-aware-trait",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/mpdf/psr-log-aware-trait.git",
"reference": "a633da6065e946cc491e1c962850344bb0bf3e78"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/a633da6065e946cc491e1c962850344bb0bf3e78",
"reference": "a633da6065e946cc491e1c962850344bb0bf3e78",
"shasum": ""
},
"require": {
"psr/log": "^3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Mpdf\\PsrLogAwareTrait\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Dorison",
"email": "mark@chromatichq.com"
},
{
"name": "Kristofer Widholm",
"email": "kristofer@chromatichq.com"
}
],
"description": "Trait to allow support of different psr/log versions.",
"support": {
"issues": "https://github.com/mpdf/psr-log-aware-trait/issues",
"source": "https://github.com/mpdf/psr-log-aware-trait/tree/v3.0.0"
},
"time": "2023-05-03T06:19:36+00:00"
},
{
"name": "myclabs/deep-copy",
"version": "1.13.4",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"doctrine/collections": "<1.6.8",
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
"phpspec/prophecy": "^1.10",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
"autoload": {
"files": [
"src/DeepCopy/deep_copy.php"
],
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Create deep copies (clones) of your objects",
"keywords": [
"clone",
"copy",
"duplicate",
"object",
"object graph"
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2025-08-01T08:46:24+00:00"
},
{
"name": "namshi/jose",
"version": "7.2.3",
@@ -2655,6 +2888,56 @@
],
"time": "2025-10-18T11:10:27+00:00"
},
{
"name": "paragonie/random_compat",
"version": "v9.99.100",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
"shasum": ""
},
"require": {
"php": ">= 7"
},
"require-dev": {
"phpunit/phpunit": "4.*|5.*",
"vimeo/psalm": "^1"
},
"suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
},
"type": "library",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com"
}
],
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
"keywords": [
"csprng",
"polyfill",
"pseudorandom",
"random"
],
"support": {
"email": "info@paragonie.com",
"issues": "https://github.com/paragonie/random_compat/issues",
"source": "https://github.com/paragonie/random_compat"
},
"time": "2020-10-15T08:29:30+00:00"
},
{
"name": "php-open-source-saver/jwt-auth",
"version": "2.8.0",
@@ -3515,6 +3798,78 @@
},
"time": "2025-09-04T20:59:21+00:00"
},
{
"name": "setasign/fpdi",
"version": "v2.6.4",
"source": {
"type": "git",
"url": "https://github.com/Setasign/FPDI.git",
"reference": "4b53852fde2734ec6a07e458a085db627c60eada"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada",
"reference": "4b53852fde2734ec6a07e458a085db627c60eada",
"shasum": ""
},
"require": {
"ext-zlib": "*",
"php": "^7.1 || ^8.0"
},
"conflict": {
"setasign/tfpdf": "<1.31"
},
"require-dev": {
"phpunit/phpunit": "^7",
"setasign/fpdf": "~1.8.6",
"setasign/tfpdf": "~1.33",
"squizlabs/php_codesniffer": "^3.5",
"tecnickcom/tcpdf": "^6.8"
},
"suggest": {
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
},
"type": "library",
"autoload": {
"psr-4": {
"setasign\\Fpdi\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jan Slabon",
"email": "jan.slabon@setasign.com",
"homepage": "https://www.setasign.com"
},
{
"name": "Maximilian Kresse",
"email": "maximilian.kresse@setasign.com",
"homepage": "https://www.setasign.com"
}
],
"description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.",
"homepage": "https://www.setasign.com/fpdi",
"keywords": [
"fpdf",
"fpdi",
"pdf"
],
"support": {
"issues": "https://github.com/Setasign/FPDI/issues",
"source": "https://github.com/Setasign/FPDI/tree/v2.6.4"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
"type": "tidelift"
}
],
"time": "2025-08-05T09:57:14+00:00"
},
{
"name": "symfony/clock",
"version": "v7.3.0",
@@ -6751,66 +7106,6 @@
},
"time": "2024-05-16T03:13:13+00:00"
},
{
"name": "myclabs/deep-copy",
"version": "1.13.4",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"doctrine/collections": "<1.6.8",
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
"phpspec/prophecy": "^1.10",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
"autoload": {
"files": [
"src/DeepCopy/deep_copy.php"
],
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Create deep copies (clones) of your objects",
"keywords": [
"clone",
"copy",
"duplicate",
"object",
"object graph"
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2025-08-01T08:46:24+00:00"
},
{
"name": "nunomaduro/collision",
"version": "v8.8.2",

View File

@@ -0,0 +1,70 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateInvoicesTable extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('invoices', function (Blueprint $table) {
$table->id();
// Links
$table->unsignedBigInteger('order_id')->index();
$table->unsignedBigInteger('customer_id')->nullable()->index(); // snapshot link if available
$table->string('mark_no')->nullable()->index();
// Invoice identity
$table->string('invoice_number')->unique();
$table->date('invoice_date')->nullable();
$table->date('due_date')->nullable();
// Payment / status
$table->string('payment_method')->nullable();
$table->string('reference_no')->nullable();
$table->enum('status', ['pending','paid','overdue'])->default('pending');
// Amounts
$table->decimal('final_amount', 14, 2)->default(0.00); // editable by user
$table->decimal('gst_percent', 5, 2)->default(0.00); // editable by user
$table->decimal('gst_amount', 14, 2)->default(0.00); // auto-calculated
$table->decimal('final_amount_with_gst', 14, 2)->default(0.00); // auto-calculated
// Customer snapshot (immutable fields)
$table->string('customer_name')->nullable();
$table->string('company_name')->nullable();
$table->string('customer_email')->nullable();
$table->string('customer_mobile')->nullable();
$table->text('customer_address')->nullable();
$table->string('pincode')->nullable();
// PDF / notes
$table->string('pdf_path')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
// Foreign keys (optional — adjust table names/namespaces if yours are different)
$table->foreign('order_id')->references('id')->on('orders')->onDelete('cascade');
// customer_id may reference users table, keep nullable to avoid migration order issues
$table->foreign('customer_id')->references('id')->on('users')->onDelete('set null');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('invoices', function (Blueprint $table) {
$table->dropForeign(['order_id']);
$table->dropForeign(['customer_id']);
});
Schema::dropIfExists('invoices');
}
}

View File

@@ -0,0 +1,52 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateInvoiceItemsTable extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('invoice_items', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('invoice_id')->index();
// Snapshot of order item fields (not editable)
$table->text('description')->nullable();
$table->integer('ctn')->default(0);
$table->integer('qty')->default(0);
$table->integer('ttl_qty')->default(0);
$table->string('unit')->nullable();
$table->decimal('price', 14, 2)->default(0.00);
$table->decimal('ttl_amount', 14, 2)->default(0.00);
$table->decimal('cbm', 12, 3)->default(0.000);
$table->decimal('ttl_cbm', 12, 3)->default(0.000);
$table->decimal('kg', 12, 3)->default(0.000);
$table->decimal('ttl_kg', 12, 3)->default(0.000);
$table->string('shop_no')->nullable();
$table->timestamps();
// FK
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('invoice_items', function (Blueprint $table) {
$table->dropForeign(['invoice_id']);
});
Schema::dropIfExists('invoice_items');
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -1,12 +1,110 @@
@extends('admin.layouts.app')
@section('page-title', 'Dashboard')
@section('page-title', 'Invoice List')
@section('content')
<div class="card shadow-sm">
<div class="card-body">
<h4>Welcome to the Admin invoice</h4>
<p>Here you can manage all system modules.</p>
<div class="card-header bg-light">
<h4 class="mb-0">All Invoices</h4>
</div>
<div class="card-body table-responsive">
<table class="table table-bordered table-striped align-middle text-center">
<thead class="table-light">
<tr>
<th>#</th>
<th>Invoice Number</th>
<th>Customer</th>
<th>Final Amount ()</th>
<th>GST %</th>
<th>Total w/GST ()</th>
<th>Status</th>
<th>Invoice Date</th>
<th>Due Date</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@foreach($invoices as $i => $invoice)
<tr>
<td>{{ $i + 1 }}</td>
<td>
<a href="#"
class="text-primary fw-bold open-invoice-popup"
data-id="{{ $invoice->id }}">
{{ $invoice->invoice_number }}
</a>
</td>
<td>{{ $invoice->customer_name }}</td>
<td>{{ number_format($invoice->final_amount, 2) }}</td>
<td>{{ $invoice->gst_percent }}%</td>
<td>{{ number_format($invoice->final_amount_with_gst, 2) }}</td>
<td>
<span class="badge
@if($invoice->status=='paid') bg-success
@elseif($invoice->status=='overdue') bg-danger
@else bg-warning text-dark @endif">
{{ ucfirst($invoice->status) }}
</span>
</td>
<td>{{ $invoice->invoice_date }}</td>
<td>{{ $invoice->due_date }}</td>
<td>
<a href="{{ route('admin.invoices.edit', $invoice->id) }}"
class="btn btn-sm btn-primary">
Edit
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
{{-- POPUP MODAL --}}
<div class="modal fade" id="invoiceModal" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Invoice Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="invoiceModalBody">
<p class="text-center text-muted">Loading...</p>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('click', function(e) {
if (e.target.closest('.open-invoice-popup')) {
e.preventDefault();
const id = e.target.closest('.open-invoice-popup').dataset.id;
const modal = new bootstrap.Modal(document.getElementById('invoiceModal'));
document.getElementById('invoiceModalBody').innerHTML =
"<p class='text-center text-muted'>Loading...</p>";
modal.show();
fetch(`/admin/invoices/${id}/popup`)
.then(res => res.text())
.then(html => {
document.getElementById('invoiceModalBody').innerHTML = html;
});
}
});
</script>
@endsection

View File

@@ -0,0 +1,89 @@
@extends('admin.layouts.app')
@section('page-title', 'Edit Invoice')
@section('content')
<div class="card shadow-sm">
<div class="card-header bg-light">
<h4>Edit Invoice</h4>
</div>
<div class="card-body">
<form action="{{ route('admin.invoices.update', $invoice->id) }}" method="POST">
@csrf
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Invoice Date</label>
<input type="date" class="form-control"
name="invoice_date"
value="{{ $invoice->invoice_date }}" required>
</div>
<div class="col-md-4">
<label class="form-label">Due Date</label>
<input type="date" class="form-control"
name="due_date"
value="{{ $invoice->due_date }}" required>
</div>
<div class="col-md-4">
<label class="form-label">Payment Method</label>
<input type="text" class="form-control"
name="payment_method"
value="{{ $invoice->payment_method }}">
</div>
<div class="col-md-4">
<label class="form-label">Reference No</label>
<input type="text" class="form-control"
name="reference_no"
value="{{ $invoice->reference_no }}">
</div>
<div class="col-md-4">
<label class="form-label">Final Amount (Editable)</label>
<input type="number" step="0.01" class="form-control"
name="final_amount"
value="{{ $invoice->final_amount }}" required>
</div>
<div class="col-md-4">
<label class="form-label">GST Percentage</label>
<input type="number" step="0.01" class="form-control"
name="gst_percent"
value="{{ $invoice->gst_percent }}" required>
</div>
<div class="col-md-4">
<label class="form-label">Status</label>
<select class="form-select" name="status" required>
<option value="pending" @selected($invoice->status=='pending')>Pending</option>
<option value="paid" @selected($invoice->status=='paid')>Paid</option>
<option value="overdue" @selected($invoice->status=='overdue')>Overdue</option>
</select>
</div>
<div class="col-md-12">
<label class="form-label">Notes</label>
<textarea class="form-control" rows="3" name="notes">
{{ $invoice->notes }}
</textarea>
</div>
<div class="col-md-12 text-end mt-3">
<button type="submit" class="btn btn-success">
Update Invoice
</button>
</div>
</div>
</form>
</div>
</div>
@endsection

View File

@@ -182,7 +182,11 @@
<a href="{{ route('admin.dashboard') }}" class="{{ request()->routeIs('admin.dashboard') ? 'active' : '' }}"><i class="bi bi-house"></i> Dashboard</a>
<a href="{{ route('admin.shipments') }}" class="{{ request()->routeIs('admin.shipments') ? 'active' : '' }}"><i class="bi bi-truck"></i> Shipments</a>
<a href="{{ route('admin.invoice') }}" class="{{ request()->routeIs('admin.invoice') ? 'active' : '' }}"><i class="bi bi-receipt"></i> Invoice</a>
<a href="{{ route('admin.invoices.index') }}"
class="{{ request()->routeIs('admin.invoices.index') ? 'active' : '' }}">
<i class="bi bi-receipt"></i> Invoice
</a>
<a href="{{ route('admin.customers') }}" class="{{ request()->routeIs('admin.customers') ? 'active' : '' }}"><i class="bi bi-people"></i> Customers</a>
<a href="{{ route('admin.reports') }}" class="{{ request()->routeIs('admin.reports') ? 'active' : '' }}"><i class="bi bi-graph-up"></i> Reports</a>
<a href="{{ route('admin.chat_support') }}" class="{{ request()->routeIs('admin.chat_support') ? 'active' : '' }}"><i class="bi bi-chat-dots"></i> Chat Support</a>

View File

@@ -0,0 +1,169 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{ $invoice->invoice_number }}</title>
<style>
body {
font-family: sans-serif;
font-size: 13px;
color: #333;
}
.header-box {
text-align: center;
padding: 10px 0;
border-bottom: 2px solid #000;
margin-bottom: 20px;
}
.logo {
width: 120px;
margin-bottom: 8px;
}
.title {
font-size: 22px;
font-weight: bold;
color: #000;
}
.subtitle {
font-size: 14px;
margin-top: 3px;
color: #666;
}
.section-title {
font-size: 16px;
font-weight: bold;
margin-top: 20px;
margin-bottom: 6px;
color: #222;
}
.info-box {
border: 1px solid #ccc;
padding: 10px;
line-height: 1.6;
background: #f9f9f9;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 12px;
}
th {
background: #efefef;
padding: 6px;
font-size: 13px;
font-weight: bold;
border: 1px solid #444;
}
td {
padding: 6px;
border: 1px solid #444;
font-size: 12px;
}
.totals-box {
margin-top: 20px;
padding: 10px;
border: 1px solid #bbb;
background: #fafafa;
font-size: 14px;
line-height: 1.8;
}
</style>
</head>
<body>
<!-- HEADER -->
<div class="header-box">
<!-- LOGO -->
<img src="{{ public_path('images/kent_logo2.png') }}" class="logo">
<div class="title">KENT LOGISTICS</div>
<div class="subtitle">Official Invoice</div>
</div>
<!-- INVOICE INFO -->
<div class="info-box">
<strong>Invoice No:</strong> {{ $invoice->invoice_number }} <br>
<strong>Invoice Date:</strong> {{ $invoice->invoice_date }} <br>
<strong>Due Date:</strong> {{ $invoice->due_date }} <br>
<strong>Status:</strong> {{ ucfirst($invoice->status) }}
</div>
<!-- CUSTOMER DETAILS -->
<div class="section-title">Customer Details</div>
<div class="info-box">
<strong>{{ $invoice->customer_name }}</strong><br>
{{ $invoice->company_name }} <br>
{{ $invoice->customer_mobile }} <br>
{{ $invoice->customer_email }} <br>
{{ $invoice->customer_address }}
</div>
<!-- ITEMS TABLE -->
<div class="section-title">Invoice Items</div>
<table>
<thead>
<tr>
<th>#</th>
<th>Description</th>
<th>CTN</th>
<th>QTY</th>
<th>TTL/QTY</th>
<th>Unit</th>
<th>Price</th>
<th>TTL Amount</th>
<th>CBM</th>
<th>TTL CBM</th>
<th>KG</th>
<th>TTL KG</th>
<th>Shop No</th>
</tr>
</thead>
<tbody>
@foreach($invoice->items as $i => $item)
<tr>
<td>{{ $i + 1 }}</td>
<td>{{ $item->description }}</td>
<td>{{ $item->ctn }}</td>
<td>{{ $item->qty }}</td>
<td>{{ $item->ttl_qty }}</td>
<td>{{ $item->unit }}</td>
<td>{{ number_format($item->price, 2) }}</td>
<td>{{ number_format($item->ttl_amount, 2) }}</td>
<td>{{ $item->cbm }}</td>
<td>{{ $item->ttl_cbm }}</td>
<td>{{ $item->kg }}</td>
<td>{{ $item->ttl_kg }}</td>
<td>{{ $item->shop_no }}</td>
</tr>
@endforeach
</tbody>
</table>
<!-- TOTALS -->
<div class="section-title">Totals</div>
<div class="totals-box">
<strong>Amount:</strong> {{ number_format($invoice->final_amount, 2) }} <br>
<strong>GST ({{ $invoice->gst_percent }}%):</strong> {{ number_format($invoice->gst_amount, 2) }} <br>
<strong>Total With GST:</strong> <strong>{{ number_format($invoice->final_amount_with_gst, 2) }}</strong>
</div>
</body>
</html>

View File

@@ -0,0 +1,73 @@
<div>
<h4 class="fw-bold mb-2">
Invoice: {{ $invoice->invoice_number }}
</h4>
<p><strong>Invoice Date:</strong> {{ $invoice->invoice_date }}</p>
<p><strong>Due Date:</strong> {{ $invoice->due_date }}</p>
<hr>
<h5>Customer Details</h5>
<p>
<strong>{{ $invoice->customer_name }}</strong><br>
{{ $invoice->company_name }}<br>
{{ $invoice->customer_mobile }}<br>
{{ $invoice->customer_email }}<br>
{{ $invoice->customer_address }}
</p>
<hr>
<h5>Invoice Items</h5>
<div class="table-responsive">
<table class="table table-bordered align-middle text-center">
<thead class="table-light">
<tr>
<th>#</th>
<th>Description</th>
<th>CTN</th>
<th>QTY</th>
<th>TTL/QTY</th>
<th>Unit</th>
<th>Price</th>
<th>TTL Amount</th>
<th>CBM</th>
<th>TTL CBM</th>
<th>KG</th>
<th>TTL KG</th>
<th>Shop No</th>
</tr>
</thead>
<tbody>
@foreach($invoice->items as $i => $item)
<tr>
<td>{{ $i+1 }}</td>
<td>{{ $item->description }}</td>
<td>{{ $item->ctn }}</td>
<td>{{ $item->qty }}</td>
<td>{{ $item->ttl_qty }}</td>
<td>{{ $item->unit }}</td>
<td>{{ number_format($item->price,2) }}</td>
<td>{{ number_format($item->ttl_amount,2) }}</td>
<td>{{ $item->cbm }}</td>
<td>{{ $item->ttl_cbm }}</td>
<td>{{ $item->kg }}</td>
<td>{{ $item->ttl_kg }}</td>
<td>{{ $item->shop_no }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<hr>
<h5>Final Summary</h5>
<p><strong>Amount:</strong> {{ number_format($invoice->final_amount,2) }}</p>
<p><strong>GST ({{ $invoice->gst_percent }}%):</strong> {{ number_format($invoice->gst_amount,2) }}</p>
<p><strong>Total With GST:</strong> {{ number_format($invoice->final_amount_with_gst,2) }}</p>
<p><strong>Status:</strong> {{ ucfirst($invoice->status) }}</p>
</div>

View File

@@ -6,6 +6,7 @@ use App\Http\Controllers\Admin\UserRequestController;
use App\Http\Controllers\Admin\AdminMarkListController;
use App\Http\Controllers\Admin\AdminOrderController;
use App\Http\Controllers\Admin\ShipmentController;
use App\Http\Controllers\Admin\AdminInvoiceController;
// -------------------------
// Default Front Page
@@ -31,7 +32,7 @@ Route::prefix('admin')->middleware('auth:admin')->group(function () {
// Route::get('/dashboard', fn() => view('admin.dashboard'))->name('admin.dashboard');
Route::get('/dashboard', [AdminOrderController::class, 'index'])->name('admin.dashboard');
//Route::get('/shipments', fn() => view('admin.shipments'))->name('admin.shipments');
Route::get('/invoice', fn() => view('admin.invoice'))->name('admin.invoice');
//Route::get('/invoice', fn() => view('admin.invoice'))->name('admin.invoice');
Route::get('/customers', fn() => view('admin.customers'))->name('admin.customers');
Route::get('/reports', fn() => view('admin.reports'))->name('admin.reports');
Route::get('/chat-support', fn() => view('admin.chat_support'))->name('admin.chat_support');
@@ -95,4 +96,23 @@ Route::prefix('admin')->middleware('auth:admin')->group(function () {
Route::get('/shipments/{id}', [ShipmentController::class, 'show'])
->name('admin.shipments.show');
// Invoice list page
Route::get('/invoices', [AdminInvoiceController::class, 'index'])
->name('admin.invoices.index');
// Popup data
Route::get('/invoices/{id}/popup', [AdminInvoiceController::class, 'popup'])
->name('admin.invoices.popup');
// Edit invoice
Route::get('/invoices/{id}/edit', [AdminInvoiceController::class, 'edit'])
->name('admin.invoices.edit');
// Update invoice
Route::post('/invoices/{id}/update', [AdminInvoiceController::class, 'update'])
->name('admin.invoices.update');
});