This commit is contained in:
Abhishek Mali
2025-11-25 13:14:53 +05:30
parent a14fe614e5
commit bebe0711f4
18 changed files with 1105 additions and 623 deletions

View File

@@ -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,7 +20,7 @@ class AdminInvoiceController extends Controller
// -------------------------------------------------------------
public function index()
{
$invoices = Invoice::latest()->get();
$invoices = Invoice::with(['order.shipments'])->latest()->get();
return view('admin.invoice', compact('invoices'));
}
@@ -24,49 +29,99 @@ class AdminInvoiceController extends Controller
// -------------------------------------------------------------
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)
{
Log::info("🟡 Invoice Update Request Received", [
'invoice_id' => $id,
'request' => $request->all()
]);
$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',
'tax_type' => 'required|in:gst,igst',
'tax_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;
Log::info("✅ Validated Invoice Update Data", $data);
$data['gst_amount'] = $gst_amount;
$data['final_amount_with_gst'] = $final_amount_with_gst;
$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;
// ✅ 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'],
]);
// Update DB
$invoice->update($data);
// Generate PDF
Log::info("✅ Invoice Updated Successfully", [
'invoice_id' => $invoice->id
]);
// regenerate PDF
$this->generateInvoicePDF($invoice);
return redirect()
@@ -74,49 +129,133 @@ class AdminInvoiceController extends Controller
->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
]);
}
}

View File

@@ -22,9 +22,15 @@ class Invoice extends Model
'reference_no',
'status',
'final_amount',
'gst_percent',
'gst_amount',
'final_amount', // without tax
'tax_type', // gst / igst
'gst_percent', // only used for gst UI input
'cgst_percent',
'sgst_percent',
'igst_percent',
'gst_amount', // total tax amount
'final_amount_with_gst',
'customer_name',
@@ -38,6 +44,7 @@ class Invoice extends Model
'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);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class InvoiceInstallment extends Model
{
use HasFactory;
protected $fillable = [
'invoice_id',
'installment_date',
'payment_method',
'reference_no',
'amount',
];
public function invoice()
{
return $this->belongsTo(Invoice::class);
}
}

View File

@@ -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');
}
}

View File

@@ -0,0 +1,53 @@
<?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) {
// GST type — gst or igst
$table->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',
]);
});
}
};

View File

@@ -0,0 +1,30 @@
<?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('invoice_installments', function (Blueprint $table) {
$table->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');
}
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -3,360 +3,225 @@
@section('page-title', 'Edit Invoice')
@section('content')
<style>
/* World-Class UI Design System - No Blur Effects */
/* --------------------------------------------------
GLOBAL VARIABLES
-------------------------------------------------- */
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--success-gradient: linear-gradient(135deg, #10b981 0%, #059669 100%);
--glass-bg: #ffffff;
--glass-border: rgba(255, 255, 255, 0.2);
--primary-gradient: linear-gradient(135deg,#667eea,#764ba2);
--success-gradient: linear-gradient(135deg,#10b981,#059669);
--glass-bg: #fff;
--shadow-soft: 0 8px 32px rgba(0,0,0,0.1);
--shadow-medium: 0 12px 48px rgba(0,0,0,0.15);
--shadow-strong: 0 20px 60px rgba(0,0,0,0.2);
}
/* Enhanced Card - No Blur */
/* --------------------------------------------------
CARD DESIGN
-------------------------------------------------- */
.card {
background: var(--glass-bg);
border-radius: 24px;
box-shadow: var(--shadow-strong);
border-radius: 12px;
border: 1px solid #e4e6ef;
animation: cardEntrance 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
box-shadow: var(--shadow-strong);
position: relative;
overflow: hidden;
animation: fadeUp 0.8s ease;
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
content:"";
position:absolute; inset:0;
background:linear-gradient(45deg,
rgba(102, 126, 234, 0.03) 0%,
rgba(118, 75, 162, 0.03) 50%,
rgba(16, 185, 129, 0.03) 100%);
rgba(102,126,234,.03),
rgba(118,75,162,.03) 50%,
rgba(16,185,129,.03));
pointer-events:none;
}
@keyframes cardEntrance {
0% {
opacity: 0;
transform: translateY(30px) scale(0.95);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
@keyframes fadeUp {
0% {opacity:0; transform:translateY(30px) scale(.95);}
100% {opacity:1; transform:translateY(0) scale(1);}
}
/* Premium Card Header */
/* --------------------------------------------------
CARD HEADER
-------------------------------------------------- */
.card-header {
background:var(--primary-gradient);
color: white;
border-bottom: none;
padding: 28px 35px;
color:#fff;
padding:11px 19px;
border:none;
position:relative;
overflow:hidden;
}
.card-header::before {
content: '';
content:"";
position:absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg,
transparent 0%,
rgba(255, 255, 255, 0.1) 50%,
transparent 100%);
animation: headerShimmer 6s infinite linear;
transform: rotate(45deg);
top:-50%; left:-50%;
width:200%; height:200%;
background:linear-gradient(45deg,transparent,
rgba(255,255,255,.1) 50%,transparent);
animation: shimmer 6s infinite linear;
}
@keyframes headerShimmer {
0% { transform: translateX(-100%) rotate(45deg); }
100% { transform: translateX(100%) rotate(45deg); }
@keyframes shimmer {
from {transform:translateX(-100%) rotate(45deg);}
to {transform:translateX(100%) rotate(45deg);}
}
.card-header h4 {
margin: 0;
font-weight: 800;
font-size: 1.6rem;
position: relative;
display: flex;
align-items: center;
gap: 12px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin:0; font-weight:800; font-size:1.6rem;
display:flex; align-items:center; gap:12px;
text-shadow:0 2px 4px rgba(0,0,0,.1);
}
.card-header h4::before {
content: '✨';
font-size: 1.4rem;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
content:""; font-size:1.4rem;
}
.card-body {
padding: 35px;
background: #f8fafc;
position: relative;
}
/* --------------------------------------------------
BODY + FORM ELEMENTS
-------------------------------------------------- */
.card-body { padding:15px; background:#f8fafc; }
/* World-Class Form Elements - No Blur */
.form-label {
font-weight: 700;
color: #1e293b;
margin-bottom: 10px;
font-size: 0.95rem;
display: flex;
align-items: center;
gap: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
position: relative;
font-weight:700; color:#1e293b;
margin-bottom:10px; font-size:.95rem;
letter-spacing:.5px; text-transform:uppercase;
display:flex; align-items:center; gap:8px;
}
.form-label::before {
content: '';
width: 4px;
height: 16px;
content:""; width:4px; height:16px;
background:var(--primary-gradient);
border-radius:2px;
display: inline-block;
}
.form-control, .form-select {
border: 2px solid #e2e8f0;
border-radius: 16px;
padding: 16px 20px;
padding: 8px 15px;
border-radius: 10px;
background: #fff;
font-size: 1rem;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
background: #ffffff;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
position: relative;
transition: .4s;
box-shadow: 0 2px 12px rgba(0, 0, 0, .05);
}
.form-control:focus, .form-select:focus {
border-color:#667eea;
box-shadow:
0 0 0 4px rgba(102, 126, 234, 0.15),
0 8px 24px rgba(102, 126, 234, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
background: #ffffff;
0 0 0 4px rgba(102,126,234,.15),
0 8px 24px rgba(102,126,234,.2);
transform:translateY(-2px) scale(1.02);
animation: inputGlow 2s infinite;
}
@keyframes inputGlow {
0%, 100% { box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.15), 0 8px 24px rgba(102, 126, 234, 0.2); }
50% { box-shadow: 0 0 0 6px rgba(102, 126, 234, 0.1), 0 12px 32px rgba(102, 126, 234, 0.25); }
}
/* hover */
.form-control:hover, .form-select:hover {
border-color:#cbd5e1;
transform:translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
box-shadow:0 6px 20px rgba(0,0,0,.08);
}
/* Enhanced Grid System */
.row.g-3 {
margin: -15px;
}
.row.g-3 > [class*="col-"] {
padding: 15px;
animation: formElementEntrance 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
@keyframes formElementEntrance {
0% {
opacity: 0;
transform: translateY(25px) scale(0.9);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* Staggered Animation Delays */
.row.g-3 > [class*="col-"]:nth-child(1) { animation-delay: 0.1s; }
.row.g-3 > [class*="col-"]:nth-child(2) { animation-delay: 0.2s; }
.row.g-3 > [class*="col-"]:nth-child(3) { animation-delay: 0.3s; }
.row.g-3 > [class*="col-"]:nth-child(4) { animation-delay: 0.4s; }
.row.g-3 > [class*="col-"]:nth-child(5) { animation-delay: 0.5s; }
.row.g-3 > [class*="col-"]:nth-child(6) { animation-delay: 0.6s; }
.row.g-3 > [class*="col-"]:nth-child(7) { animation-delay: 0.7s; }
.row.g-3 > [class*="col-"]:nth-child(8) { animation-delay: 0.8s; }
/* Premium Textarea */
/* textarea */
textarea.form-control {
height: 65px;
resize: vertical;
min-height: 120px;
line-height: 1.6;
background: #ffffff;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
textarea.form-control:focus {
transform: translateY(-2px) scale(1.01);
box-shadow:
0 0 0 4px rgba(102, 126, 234, 0.15),
0 12px 32px rgba(102, 126, 234, 0.2);
}
/* World-Class Button Design - No Blur */
/* --------------------------------------------------
BUTTON STYLING
-------------------------------------------------- */
.btn-success {
background: var(--success-gradient);
border: none;
padding: 18px 45px;
border-radius: 16px;
color: white;
padding: 10px 25px;
border-radius: 7px;
font-weight: 700;
font-size: 1.1rem;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
color: #fff;
transition: .4s;
box-shadow: var(--shadow-medium);
position: relative;
overflow: hidden;
}
.btn-success::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg,
transparent,
rgba(255, 255, 255, 0.4),
transparent);
transition: left 0.6s ease;
}
.btn-success:hover::before {
left: 100%;
content:"";
position:absolute; left:-100%; top:0;
width:100%; height:100%;
background:linear-gradient(90deg,transparent,
rgba(255,255,255,.4),transparent);
transition:.6s;
}
.btn-success:hover {
transform:translateY(-4px) scale(1.05);
box-shadow:
0 16px 40px rgba(16, 185, 129, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.1);
background: var(--success-gradient);
box-shadow:0 16px 40px rgba(16,185,129,.4);
}
.btn-success:active {
transform: translateY(-1px) scale(1.02);
transition: all 0.1s ease;
}
.btn-success:hover::before { left:100%; }
/* Enhanced Select Styling */
.form-select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23667eea' stroke-linecap='round' stroke-linejoin='round' stroke-width='2.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 20px center;
background-repeat: no-repeat;
background-size: 18px;
padding-right: 50px;
cursor: pointer;
appearance: none;
}
/* Advanced Loading Animation */
.btn-success.loading {
pointer-events: none;
padding-right: 60px;
pointer-events:none; padding-right:60px;
}
.btn-success.loading::after {
content: '';
position: absolute;
right: 20px;
top: 50%;
width: 20px;
height: 20px;
margin-top: -10px;
border: 2px solid transparent;
border-top: 2px solid white;
content:"";
position:absolute; right:20px; top:50%;
width:20px; height:20px; margin-top:-10px;
border-radius:50%;
border:2px solid transparent;
border-top-color:#fff;
animation:spin 1s linear infinite;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes spin {to{transform:rotate(360deg);}}
/* Premium Focus States */
.form-control:focus-visible,
.form-select:focus-visible,
.btn-success:focus-visible {
outline: 3px solid #667eea;
outline-offset: 2px;
border-radius: 8px;
}
/* Advanced Responsive Design */
/* --------------------------------------------------
RESPONSIVE
-------------------------------------------------- */
@media(max-width:768px){
.card-body {
padding: 25px 20px;
.card-body{padding:25px 20px;}
.card-header{padding:22px 25px;}
.card-header h4{font-size:1.4rem;}
.btn-success{width:100%; padding:16px 30px;}
.form-control,.form-select{padding:14px 16px;}
}
.card-header {
padding: 22px 25px;
}
.card-header h4 {
font-size: 1.4rem;
}
.btn-success {
width: 100%;
padding: 16px 30px;
font-size: 1rem;
}
.form-control, .form-select {
padding: 14px 16px;
}
}
/* Micro-interactions for Enhanced UX */
.form-control:valid {
border-left: 3px solid #10b981;
}
.form-control:invalid:not(:focus):not(:placeholder-shown) {
border-left: 3px solid #ef4444;
animation: shake 0.5s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
/* Smooth scrolling for better UX */
html {
scroll-behavior: smooth;
}
/* Performance optimized animations */
/* Only animation reduction */
@media(prefers-reduced-motion:reduce){
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
*{animation:none!important; transition:none!important;}
}
</style>
<div class="card shadow-sm">
{{-- Invoice Preview Section --}}
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h4 class="fw-bold mb-0">
<i class="fas fa-file-invoice me-2"></i> Invoice Details
</h4>
</div>
<div class="card-body">
@include('admin.popup_invoice', [
'invoice' => $invoice,
'shipment' => $shipment,
'embedded' => true
])
</div>
</div>
<!-- --------------------------------------------------
HTML CONTENT (UNCHANGED)
-------------------------------------------------- -->
<div class="card shadow-sm">
<div class="card-header">
<h4>Edit Invoice</h4>
</div>
@@ -368,44 +233,53 @@ html {
<div class="col-md-4">
<label class="form-label">Invoice Date</label>
<input type="date" class="form-control"
name="invoice_date"
<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"
<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"
<label class="form-label">Final Amount ()</label>
<input type="number" step="0.01" class="form-control" name="final_amount"
value="{{ $invoice->final_amount }}" required>
</div>
<!-- TAX TYPE -->
<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>
<label class="form-label d-block">Tax Type</label>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio"
name="tax_type" value="gst"
@checked($invoice->tax_type === 'gst')>
<label class="form-check-label">GST (CGST+SGST)</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio"
name="tax_type" value="igst"
@checked($invoice->tax_type === 'igst')>
<label class="form-check-label">IGST</label>
</div>
</div>
<!-- One unified input -->
<div class="col-md-4">
<label class="form-label">Tax Percentage (%)</label>
<input type="number" step="0.01" min="0" max="28"
class="form-control"
name="tax_percent"
value="{{ old('tax_percent',
$invoice->tax_type == 'gst'
? $invoice->cgst_percent + $invoice->sgst_percent
: $invoice->igst_percent
) }}"
required>
</div>
<div class="col-md-4">
@@ -419,14 +293,79 @@ html {
<div class="col-md-12">
<label class="form-label">Notes</label>
<textarea class="form-control" rows="3" name="notes">
{{ $invoice->notes }}
</textarea>
<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 type="submit" class="btn btn-success">Update Invoice</button>
</div>
</div>
</form>
</div>
</div>
@php
$totalPaid = $invoice->installments->sum('amount');
$remaining = $invoice->final_amount_with_gst - $totalPaid;
@endphp
<hr>
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="fw-bold mb-0">Installment Payments</h3>
@if($remaining > 0)
<button id="toggleInstallmentForm" class="btn btn-success">
+ Add Installment
</button>
@endif
</div>
<!-- Installment Form -->
<div id="installmentForm" class="card shadow-sm d-none mb-4">
<div class="card-header">
<h4>Add Installment</h4>
</div>
<div class="card-body">
<form id="installmentSubmitForm">
@csrf
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Installment Date</label>
<input type="date" name="installment_date" class="form-control" required>
</div>
<div class="col-md-4">
<label class="form-label">Payment Method</label>
<select name="payment_method" class="form-select" required>
<option value="cash">Cash</option>
<option value="bank">Bank Transfer</option>
<option value="upi">UPI</option>
<option value="cheque">Cheque</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Reference No (optional)</label>
<input type="text" name="reference_no" class="form-control">
</div>
<div class="col-md-12">
<label class="form-label">Amount</label>
<input type="number" name="amount" id="installmentAmount" class="form-control"
step="0.01" min="1" required>
</div>
<div class="col-md-12 text-end mt-2">
<button type="submit" class="btn btn-success" id="installmentSubmitBtn">
Submit Installment
</button>
</div>
@@ -435,47 +374,229 @@ html {
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="d-flex justify-content-between fs-5 fw-bold">
<span>Total Amount (Before Tax):</span>
<span>{{ number_format($invoice->final_amount, 2) }}</span>
</div>
<div class="d-flex justify-content-between text-primary fs-5 fw-bold">
<span>Tax Type:</span>
<span>
@if($invoice->tax_type === 'gst')
GST (CGST + SGST)
@else
IGST
@endif
</span>
</div>
<div class="d-flex justify-content-between text-primary fs-5 fw-bold">
<span>Tax Percentage:</span>
<span>
@if($invoice->tax_type === 'gst')
{{ $invoice->cgst_percent + $invoice->sgst_percent }}%
@else
{{ $invoice->igst_percent }}%
@endif
</span>
</div>
<div class="d-flex justify-content-between text-warning fs-5 fw-bold">
<span>GST Amount:</span>
<span>{{ number_format($invoice->gst_amount, 2) }}</span>
</div>
<hr>
<div class="d-flex justify-content-between fs-4 fw-bold text-dark">
<span>Total Invoice Amount (Including GST):</span>
<span>{{ number_format($invoice->final_amount_with_gst, 2) }}</span>
</div>
<hr>
<div class="d-flex justify-content-between text-success fs-5 fw-bold">
<span>Total Paid:</span>
<span id="paidAmount">{{ number_format($totalPaid, 2) }}</span>
</div>
<div class="d-flex justify-content-between text-danger fs-5 fw-bold">
<span>Remaining:</span>
<span id="remainingAmount">{{ number_format($remaining, 2) }}</span>
</div>
</div>
</div>
<!-- Installment History -->
<div class="card shadow-sm">
<div class="card-header">
<h4>Installment History</h4>
</div>
<div class="card-body p-0">
<table class="table table-hover mb-0" id="installmentTable">
<thead style="background:#f1f5f9;">
<tr>
<th>#</th>
<th>Date</th>
<th>Payment Method</th>
<th>Reference No</th>
<th>Amount</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@foreach($invoice->installments as $i)
<tr data-id="{{ $i->id }}">
<td>{{ $loop->iteration }}</td>
<td>{{ $i->installment_date }}</td>
<td>{{ ucfirst($i->payment_method) }}</td>
<td>{{ $i->reference_no }}</td>
<td class="fw-bold text-success">{{ number_format($i->amount, 2) }}</td>
<td>
<button class="btn btn-danger btn-sm deleteInstallment">
Delete
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<script>
// World-Class Interactive Enhancements - No Parallax
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('form');
const submitBtn = document.querySelector('.btn-success');
const inputs = document.querySelectorAll('input, select, textarea');
document.addEventListener("DOMContentLoaded", function () {
// Enhanced form submission
form.addEventListener('submit', function(e) {
submitBtn.classList.add('loading');
submitBtn.innerHTML = 'Updating Invoice...';
// ✅ Toggle Installment Form
const toggleBtn = document.getElementById("toggleInstallmentForm");
const formBox = document.getElementById("installmentForm");
// Add a small delay to show loading state
setTimeout(() => {
if (!form.checkValidity()) {
submitBtn.classList.remove('loading');
submitBtn.innerHTML = 'Update Invoice';
if (toggleBtn) {
toggleBtn.addEventListener("click", () => formBox.classList.toggle("d-none"));
}
}, 100);
// ✅ Add Installment
const submitForm = document.getElementById("installmentSubmitForm");
const submitBtn = document.getElementById("installmentSubmitBtn");
const formatINR = amt =>
"" + Number(amt).toLocaleString("en-IN", {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
// Real-time validation with visual feedback
inputs.forEach(input => {
input.addEventListener('input', function() {
if (this.checkValidity()) {
this.style.borderLeftColor = '#10b981';
} else {
this.style.borderLeftColor = '#ef4444';
submitForm.addEventListener("submit", function (e) {
e.preventDefault();
submitBtn.textContent = "Processing...";
submitBtn.disabled = true;
fetch("{{ route('admin.invoice.installment.store', $invoice->id) }}", {
method: "POST",
headers: {
"X-CSRF-TOKEN": submitForm.querySelector("input[name=_token]").value,
"Accept": "application/json"
},
body: new FormData(submitForm)
})
.then(res => res.json())
.then(data => {
submitBtn.textContent = "Submit Installment";
submitBtn.disabled = false;
if (data.status === "error") {
alert(data.message);
return;
}
const table = document.querySelector("#installmentTable tbody");
const index = table.rows.length + 1;
table.insertAdjacentHTML("beforeend", `
<tr data-id="${data.installment.id}">
<td>${index}</td>
<td>${data.installment.installment_date}</td>
<td>${data.installment.payment_method.toUpperCase()}</td>
<td>${data.installment.reference_no ?? ""}</td>
<td class="fw-bold text-success">${formatINR(data.installment.amount)}</td>
<td>
<button class="btn btn-danger btn-sm deleteInstallment"> Delete</button>
</td>
</tr>
`);
// ✅ Update totals (Gst included)
document.getElementById("paidAmount").textContent = formatINR(data.totalPaid);
document.getElementById("remainingAmount").textContent = formatINR(data.remaining);
submitForm.reset();
// ✅ If fully paid — hide Add Installment button & form
if (data.isCompleted) {
toggleBtn?.remove();
formBox.classList.add("d-none");
}
alert(data.message);
})
.catch(() => {
submitBtn.textContent = "Submit Installment";
submitBtn.disabled = false;
alert("Something went wrong");
});
});
// ✅ Delete Installment (event delegation)
document.addEventListener("click", function (e) {
if (!e.target.classList.contains("deleteInstallment")) return;
if (!confirm("Are you sure you want to delete this installment?")) return;
const row = e.target.closest("tr");
const id = row.getAttribute("data-id");
fetch("{{ url('/admin/installment') }}/" + id, {
method: "DELETE",
headers: {
"X-CSRF-TOKEN": "{{ csrf_token() }}",
"Accept": "application/json"
}
})
.then(res => res.json())
.then(data => {
if (data.status === "success") {
row.remove();
document.getElementById("paidAmount").textContent = formatINR(data.totalPaid);
document.getElementById("remainingAmount").textContent = formatINR(data.remaining);
// ✅ If remaining exists but Add Installment button disappeared → reload UI
if (data.remaining > 0 && !toggleBtn) {
location.reload();
}
// ✅ Remove entire card if no installments left
if (data.isZero) {
document.getElementById("installmentTable").closest(".card").remove();
}
alert(data.message);
}
});
// Add focus effects
input.addEventListener('focus', function() {
this.parentElement.style.transform = 'translateY(-2px)';
});
input.addEventListener('blur', function() {
this.parentElement.style.transform = 'translateY(0)';
});
});
});
</script>
@endsection

View File

@@ -164,7 +164,7 @@
}
.content-wrapper {
padding: 18px 16px 0 16px;
padding: 18px 16px 16px 16px;
flex-grow: 1;
overflow-y: auto;
}

View File

@@ -1,207 +1,280 @@
<div class="p-4">
<!-- Invoice Header -->
<div class="row mb-4">
{{-- INVOICE CONTENT (NO POPUP WRAPPERS HERE) --}}
<!-- ============================
INVOICE HEADER
============================ -->
<div class="mb-4">
<div class="row">
<div class="col-md-6">
<h2 class="fw-bold text-primary mb-1">
<i class="fas fa-file-invoice me-2"></i> INVOICE
</h2>
<h4 class="fw-bold text-dark mb-0">{{ $invoice->invoice_number }}</h4>
{{-- ORDER + SHIPMENT INFO --}}
<div class="mt-2 small">
{{-- ORDER ID --}}
@if($invoice->order_id)
<div>
<strong>Order ID:</strong>
{{ $invoice->order->order_id ?? $invoice->order_id }}
</div>
@endif
{{-- SHIPMENT ID --}}
@if(isset($shipment) && $shipment)
<div>
<strong>Shipment ID:</strong> {{ $shipment->shipment_id }}
</div>
@endif
</div>
</div>
<div class="col-md-6 text-end">
<div class="d-inline-block bg-light rounded-3 p-3">
<span class="badge
<span class="badge fs-6 px-3 py-2
@if($invoice->status=='paid') bg-success
@elseif($invoice->status=='overdue') bg-danger
@elseif($invoice->status=='pending') bg-warning text-dark
@else bg-secondary @endif
fs-6 px-3 py-2">
@else bg-secondary @endif">
<i class="fas
@if($invoice->status=='paid') fa-check-circle
@elseif($invoice->status=='overdue') fa-exclamation-circle
@elseif($invoice->status=='pending') fa-clock
@else fa-question-circle @endif me-1"></i>
{{ ucfirst($invoice->status) }}
</span>
</div>
</div>
</div>
<!-- Dates - Compact Professional Layout -->
<div class="row mb-3">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body py-2">
<div class="row align-items-center text-center">
<div class="col-md-5">
<div class="mb-0">
<div class="text-muted fw-semibold small">INVOICE DATE</div>
</div>
<div class="fw-bold text-dark" style="font-size: 0.95rem;">
<!-- ============================
DATES SECTION
============================ -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body py-3">
<div class="row text-center align-items-center">
<div class="col-md-5">
<small class="text-muted fw-semibold">INVOICE DATE</small>
<div class="fw-bold text-dark">
{{ \Carbon\Carbon::parse($invoice->invoice_date)->format('M d, Y') }}
</div>
</div>
<div class="col-md-2">
<div class="date-connector">
<i class="fas fa-arrow-right text-muted small"></i>
</div>
<i class="fas fa-arrow-right text-muted"></i>
</div>
<div class="col-md-5">
<div class="mb-0">
<div class="text-muted fw-semibold small">DUE DATE</div>
</div>
<div class="fw-bold @if($invoice->status == 'overdue') text-danger @else text-dark @endif" style="font-size: 0.95rem;">
<small class="text-muted fw-semibold">DUE DATE</small>
<div class="fw-bold {{ $invoice->status=='overdue' ? 'text-danger' : 'text-dark' }}">
{{ \Carbon\Carbon::parse($invoice->due_date)->format('M d, Y') }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Customer Details -->
<div class="row mb-4">
<div class="col-12">
<div class="card border-0 shadow-sm">
<!-- ============================
CUSTOMER DETAILS
============================ -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-light py-2">
<h6 class="mb-0 fw-bold text-dark">
<i class="fas fa-user me-2"></i>Customer Details
</h6>
<h6 class="fw-bold mb-0"><i class="fas fa-user me-2"></i> Customer Details</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6 class="fw-bold text-primary mb-1">{{ $invoice->customer_name }}</h6>
<h6 class="fw-bold text-primary">{{ $invoice->customer_name }}</h6>
@if($invoice->company_name)
<p class="mb-1">
<strong>Company:</strong> {{ $invoice->company_name }}
</p>
<p class="mb-1"><strong>Company:</strong> {{ $invoice->company_name }}</p>
@endif
<p class="mb-1">
<strong>Mobile:</strong> {{ $invoice->customer_mobile }}
</p>
<p class="mb-1">
<strong>Email:</strong> {{ $invoice->customer_email }}
</p>
</div>
<div class="col-md-6">
<p class="mb-0">
<strong>Address:</strong><br>
{{ $invoice->customer_address }}
</p>
</div>
</div>
</div>
</div>
</div>
<p class="mb-1"><strong>Mobile:</strong> {{ $invoice->customer_mobile }}</p>
<p class="mb-1"><strong>Email:</strong> {{ $invoice->customer_email }}</p>
</div>
<!-- Invoice Items -->
<div class="row mb-4">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-light py-2">
<h6 class="mb-0 fw-bold text-dark">
<i class="fas fa-list me-2"></i>Invoice Items
</h6>
<div class="col-md-6">
<p class="mb-1"><strong>Address:</strong><br>{{ $invoice->customer_address }}</p>
<p class="mb-1"><strong>Pincode:</strong> {{ $invoice->pincode }}</p>
</div>
<div class="card-body p-0">
</div>
</div>
</div>
<!-- ============================
INVOICE ITEMS
============================ -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-light py-2">
<h6 class="fw-bold mb-0"><i class="fas fa-list me-2"></i> Invoice Items</h6>
</div>
<div class="table-responsive">
<table class="table table-bordered table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="text-center">#</th>
<th>#</th>
<th>Description</th>
<th class="text-center">CTN</th>
<th class="text-center">QTY</th>
<th class="text-center">TTL/QTY</th>
<th class="text-center">Unit</th>
<th class="text-center">Price</th>
<th class="text-center">TTL Amount</th>
<th class="text-center">CBM</th>
<th class="text-center">TTL CBM</th>
<th class="text-center">KG</th>
<th class="text-center">TTL KG</th>
<th class="text-center">Shop No</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 class="text-center fw-bold text-muted">{{ $i+1 }}</td>
<td class="fw-semibold">{{ $item->description }}</td>
<td class="text-center fw-bold">{{ $i + 1 }}</td>
<td>{{ $item->description }}</td>
<td class="text-center">{{ $item->ctn }}</td>
<td class="text-center">{{ $item->qty }}</td>
<td class="text-center fw-bold">{{ $item->ttl_qty }}</td>
<td class="text-center">{{ $item->ttl_qty }}</td>
<td class="text-center">{{ $item->unit }}</td>
<td class="text-center text-success fw-bold">{{ number_format($item->price,2) }}</td>
<td class="text-center text-primary fw-bold">{{ number_format($item->ttl_amount,2) }}</td>
<td class="text-center">{{ number_format($item->price,2) }}</td>
<td class="text-center fw-bold text-primary">{{ number_format($item->ttl_amount,2) }}</td>
<td class="text-center">{{ $item->cbm }}</td>
<td class="text-center">{{ $item->ttl_cbm }}</td>
<td class="text-center">{{ $item->kg }}</td>
<td class="text-center">{{ $item->ttl_kg }}</td>
<td class="text-center">
<span class="badge bg-light text-dark border">{{ $item->shop_no }}</span>
</td>
<td class="text-center">{{ $item->shop_no }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Final Summary -->
</div>
<!-- ============================
FINAL SUMMARY
============================ -->
<div class="row">
<div class="col-md-6 offset-md-6">
<div class="card border-0 bg-light">
<div class="card-header bg-dark text-white py-2">
<h6 class="mb-0 fw-bold">
<i class="fas fa-calculator me-2"></i>Final Summary
</h6>
<h6 class="fw-bold mb-0"><i class="fas fa-calculator me-2"></i> Final Summary</h6>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
<div class="d-flex justify-content-between mb-2 border-bottom pb-1">
<span class="fw-semibold">Amount:</span>
<span class="fw-bold text-dark">{{ number_format($invoice->final_amount,2) }}</span>
<span class="fw-bold">{{ number_format($invoice->final_amount, 2) }}</span>
</div>
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
<span class="fw-semibold">GST ({{ $invoice->gst_percent }}%):</span>
@if($invoice->tax_type === 'gst')
{{-- CGST --}}
<div class="d-flex justify-content-between mb-2 border-bottom pb-1">
<span class="fw-semibold">CGST ({{ $invoice->cgst_percent }}%):</span>
<span class="fw-bold text-danger">{{ number_format($invoice->gst_amount/2, 2) }}</span>
</div>
{{-- SGST --}}
<div class="d-flex justify-content-between mb-2 border-bottom pb-1">
<span class="fw-semibold">SGST ({{ $invoice->sgst_percent }}%):</span>
<span class="fw-bold text-danger">{{ number_format($invoice->gst_amount/2, 2) }}</span>
</div>
@elseif($invoice->tax_type === 'igst')
{{-- IGST --}}
<div class="d-flex justify-content-between mb-2 border-bottom pb-1">
<span class="fw-semibold">IGST ({{ $invoice->igst_percent }}%):</span>
<span class="fw-bold text-danger">{{ number_format($invoice->gst_amount, 2) }}</span>
</div>
<div class="d-flex justify-content-between align-items-center pt-1">
<span class="fw-bold text-dark">Total With GST:</span>
@endif
<div class="d-flex justify-content-between pt-1">
<span class="fw-bold text-dark">Total Payable:</span>
<span class="fw-bold text-success">{{ number_format($invoice->final_amount_with_gst, 2) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.date-connector {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #6c757d;
<!-- ============================
FOOTER DOWNLOAD & SHARE
============================ -->
<div class="modal-footer">
@if($invoice->pdf_path)
<a href="{{ asset($invoice->pdf_path) }}" class="btn btn-primary" download>
<i class="bi bi-download me-1"></i> Download PDF
</a>
<button class="btn btn-success" onclick="shareInvoice()">
<i class="bi bi-share me-1"></i> Share
</button>
@endif
<!-- <button class="btn btn-secondary" data-bs-dismiss="modal">Close</button> -->
</div>
<!-- ============================
SHARE SCRIPT
============================ -->
<script>
function shareInvoice() {
const shareData = {
title: "Invoice {{ $invoice->invoice_number }}",
text: "Sharing invoice {{ $invoice->invoice_number }}",
url: "{{ asset($invoice->pdf_path) }}"
};
if (navigator.share) {
navigator.share(shareData).catch(() => {});
} else {
navigator.clipboard.writeText(shareData.url);
alert("Link copied! Sharing not supported on this browser.");
}
.date-connector i {
background: #f8f9fa;
padding: 4px;
border-radius: 50%;
border: 1px solid #e9ecef;
}
.card {
border-radius: 6px;
}
.table {
margin-bottom: 0;
}
.table > :not(caption) > * > * {
padding: 10px 6px;
}
</style>
</script>

View File

@@ -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');