Compare commits
6 Commits
43b1a64911
...
qa
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
9a6ca49ad7 | ||
|
|
bf2689e62d | ||
|
|
c25b468c77 | ||
|
|
5d8a746876 | ||
|
|
bb2a361a97 | ||
|
|
6b5876e08f |
@@ -50,19 +50,12 @@ class AdminInvoiceController extends Controller
|
||||
// POPUP VIEW
|
||||
// -------------------------------------------------------------
|
||||
public function popup($id)
|
||||
{
|
||||
{
|
||||
$invoice = Invoice::with([
|
||||
'items',
|
||||
'chargeGroups.items',
|
||||
])->findOrFail($id);
|
||||
|
||||
// demo update असेल तर
|
||||
$invoice->update([
|
||||
'customer_email' => 'test@demo.com',
|
||||
'customer_address' => 'TEST ADDRESS',
|
||||
'pincode' => '999999',
|
||||
]);
|
||||
|
||||
$shipment = null;
|
||||
|
||||
$groupedItemIds = $invoice->chargeGroups
|
||||
@@ -72,7 +65,7 @@ class AdminInvoiceController extends Controller
|
||||
->toArray();
|
||||
|
||||
return view('admin.popup_invoice', compact('invoice', 'shipment', 'groupedItemIds'));
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// EDIT INVOICE PAGE
|
||||
@@ -87,6 +80,28 @@ class AdminInvoiceController extends Controller
|
||||
'installments',
|
||||
])->findOrFail($id);
|
||||
|
||||
// ✅ Customer details sync — जर test data आला असेल तर fix होईल
|
||||
if ($invoice->customer) {
|
||||
$needsUpdate = [];
|
||||
|
||||
if (empty($invoice->customer_email) || $invoice->customer_email === 'test@demo.com') {
|
||||
$needsUpdate['customer_email'] = $invoice->customer->email;
|
||||
}
|
||||
|
||||
if (empty($invoice->customer_address) || $invoice->customer_address === 'TEST ADDRESS') {
|
||||
$needsUpdate['customer_address'] = $invoice->customer->address;
|
||||
}
|
||||
|
||||
if (empty($invoice->pincode) || $invoice->pincode === '999999') {
|
||||
$needsUpdate['pincode'] = $invoice->customer->pincode;
|
||||
}
|
||||
|
||||
if (!empty($needsUpdate)) {
|
||||
$invoice->update($needsUpdate);
|
||||
$invoice->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
$shipment = null;
|
||||
|
||||
$groupedItemIds = $invoice->chargeGroups
|
||||
@@ -115,7 +130,7 @@ class AdminInvoiceController extends Controller
|
||||
$data = $request->validate([
|
||||
'invoice_date' => 'required|date',
|
||||
'due_date' => 'required|date|after_or_equal:invoice_date',
|
||||
'status' => 'required|in:pending,paid,overdue',
|
||||
'status' => 'required|in:pending,paying,paid,overdue',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
|
||||
@@ -134,7 +149,7 @@ class AdminInvoiceController extends Controller
|
||||
$this->generateInvoicePDF($invoice);
|
||||
|
||||
return redirect()
|
||||
->route('admin.invoices.index')
|
||||
->route('admin.invoices.edit', $invoice->id)
|
||||
->with('success', 'Invoice updated & PDF generated successfully.');
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use Illuminate\Http\Request;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Carbon\Carbon;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Illuminate\Support\Facades\Storage; // <-- added for Excel download
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ContainerController extends Controller
|
||||
{
|
||||
@@ -518,9 +518,12 @@ class ContainerController extends Controller
|
||||
$firstMark = $rowsForCustomer[0]['mark'];
|
||||
$snap = $markToSnapshot[$firstMark] ?? null;
|
||||
|
||||
// ✅ Customer User model वरून fetch करा (customer_id string आहे जसे CID-2025-000001)
|
||||
$customerUser = \App\Models\User::where('customer_id', $customerId)->first();
|
||||
|
||||
$invoice = new Invoice();
|
||||
$invoice->container_id = $container->id;
|
||||
// $invoice->customer_id = $customerId;
|
||||
$invoice->customer_id = $customerUser->id ?? null; // ✅ integer id store करतोय
|
||||
$invoice->mark_no = $firstMark;
|
||||
|
||||
$invoice->invoice_number = $this->generateInvoiceNumber();
|
||||
@@ -533,21 +536,25 @@ class ContainerController extends Controller
|
||||
->addDays(10)
|
||||
->format('Y-m-d');
|
||||
|
||||
// ✅ Snapshot data from MarkList (backward compatibility)
|
||||
if ($snap) {
|
||||
$invoice->customer_name = $snap['customer_name'] ?? null;
|
||||
$invoice->company_name = $snap['company_name'] ?? null;
|
||||
$invoice->customer_mobile = $snap['mobile_no'] ?? null;
|
||||
}
|
||||
|
||||
// ✅ User model वरून email, address, pincode घ्या
|
||||
if ($customerUser) {
|
||||
$invoice->customer_email = $customerUser->email ?? null;
|
||||
$invoice->customer_address = $customerUser->address ?? null;
|
||||
$invoice->pincode = $customerUser->pincode ?? null;
|
||||
}
|
||||
|
||||
$invoice->final_amount = 0;
|
||||
$invoice->gst_percent = 0;
|
||||
$invoice->gst_amount = 0;
|
||||
$invoice->final_amount_with_gst = 0;
|
||||
|
||||
$invoice->customer_email = null;
|
||||
$invoice->customer_address = null;
|
||||
$invoice->pincode = null;
|
||||
|
||||
$uniqueMarks = array_unique(array_column($rowsForCustomer, 'mark'));
|
||||
$invoice->notes = 'Auto-created from Container ' . $container->container_number
|
||||
. ' for Mark(s): ' . implode(', ', $uniqueMarks);
|
||||
@@ -613,7 +620,18 @@ class ContainerController extends Controller
|
||||
public function show(Container $container)
|
||||
{
|
||||
$container->load('rows');
|
||||
return view('admin.container_show', compact('container'));
|
||||
|
||||
// paid / paying invoices च्या row indexes collect करा
|
||||
$lockedRowIndexes = \App\Models\Invoice::whereIn('invoices.status', ['paid', 'paying'])
|
||||
->where('invoices.container_id', $container->id)
|
||||
->join('invoice_items', 'invoices.id', '=', 'invoice_items.invoice_id')
|
||||
->pluck('invoice_items.container_row_index')
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return view('admin.container_show', compact('container', 'lockedRowIndexes'));
|
||||
}
|
||||
|
||||
public function updateRows(Request $request, Container $container)
|
||||
@@ -871,9 +889,8 @@ class ContainerController extends Controller
|
||||
return Storage::download($path, $fileName);
|
||||
}
|
||||
|
||||
|
||||
public function popupPopup(Container $container)
|
||||
{
|
||||
{
|
||||
// existing show सारखाच data वापरू
|
||||
$container->load('rows');
|
||||
|
||||
@@ -938,6 +955,5 @@ class ContainerController extends Controller
|
||||
'container' => $container,
|
||||
'summary' => $summary,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,23 +21,33 @@ class UserOrderController extends Controller
|
||||
}
|
||||
|
||||
// -------------------------------------
|
||||
// Get all orders
|
||||
// Get customer invoices with containers
|
||||
// -------------------------------------
|
||||
$orders = $user->orders()->with('invoice')->get();
|
||||
$invoices = $user->invoices()->with('container')->get();
|
||||
|
||||
// Unique containers for this customer
|
||||
$containers = $invoices->pluck('container')->filter()->unique('id');
|
||||
|
||||
// -------------------------------------
|
||||
// Counts
|
||||
// Counts based on container status
|
||||
// -------------------------------------
|
||||
$totalOrders = $orders->count();
|
||||
$delivered = $orders->where('status', 'delivered')->count();
|
||||
$inTransit = $orders->where('status', '!=', 'delivered')->count();
|
||||
$totalOrders = $containers->count();
|
||||
|
||||
$delivered = $containers->where('status', 'delivered')->count();
|
||||
|
||||
$inTransit = $containers->whereNotIn('status', [
|
||||
'delivered',
|
||||
'warehouse',
|
||||
'domestic-distribution'
|
||||
])->count();
|
||||
|
||||
$active = $totalOrders;
|
||||
|
||||
// -------------------------------------
|
||||
// Total Amount = Invoice.total_with_gst
|
||||
// Total Amount = sum of invoice totals
|
||||
// -------------------------------------
|
||||
$totalAmount = $orders->sum(function ($o) {
|
||||
return $o->invoice->final_amount_with_gst ?? 0;
|
||||
$totalAmount = $invoices->sum(function ($invoice) {
|
||||
return $invoice->final_amount_with_gst ?? 0;
|
||||
});
|
||||
|
||||
// Format total amount in K, L, Cr
|
||||
@@ -45,13 +55,12 @@ class UserOrderController extends Controller
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
|
||||
'summary' => [
|
||||
'active_orders' => $active,
|
||||
'in_transit_orders' => $inTransit,
|
||||
'delivered_orders' => $delivered,
|
||||
'total_value' => $formattedAmount, // formatted value
|
||||
'total_raw' => $totalAmount // original value
|
||||
'total_value' => $formattedAmount,
|
||||
'total_raw' => $totalAmount
|
||||
]
|
||||
]);
|
||||
}
|
||||
@@ -90,18 +99,26 @@ class UserOrderController extends Controller
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Fetch orders for this user
|
||||
$orders = $user->orders()
|
||||
->with(['invoice', 'shipments'])
|
||||
// Get invoices with containers for this customer
|
||||
$invoices = $user->invoices()
|
||||
->with('container')
|
||||
->orderBy('id', 'desc')
|
||||
->get()
|
||||
->map(function ($o) {
|
||||
->get();
|
||||
|
||||
// Extract unique containers
|
||||
$containers = $invoices->pluck('container')
|
||||
->filter()
|
||||
->unique('id')
|
||||
->values();
|
||||
|
||||
$orders = $containers->map(function ($container) {
|
||||
|
||||
return [
|
||||
'order_id' => $o->order_id,
|
||||
'status' => $o->status,
|
||||
'amount' => $o->ttl_amount,
|
||||
'description'=> "Order from {$o->origin} to {$o->destination}",
|
||||
'created_at' => $o->created_at,
|
||||
'order_id' => $container->id,
|
||||
'container_number' => $container->container_number,
|
||||
'status' => $container->status,
|
||||
'container_date' => $container->container_date,
|
||||
'created_at' => $container->created_at,
|
||||
];
|
||||
});
|
||||
|
||||
@@ -115,45 +132,73 @@ public function orderDetails($order_id)
|
||||
{
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
$order = $user->orders()
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Find container first
|
||||
$container = \App\Models\Container::find($order_id);
|
||||
|
||||
if (!$container) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Container not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Find invoice belonging to this user for this container
|
||||
$invoice = \App\Models\Invoice::where('customer_id', $user->id)
|
||||
->where('container_id', $container->id)
|
||||
->with(['items'])
|
||||
->where('order_id', $order_id)
|
||||
->first();
|
||||
|
||||
if (!$order) {
|
||||
return response()->json(['success' => false, 'message' => 'Order not found'], 404);
|
||||
if (!$invoice) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Order not found for this user'
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'order' => $order
|
||||
'order' => [
|
||||
'container_id' => $container->id,
|
||||
'container_number' => $container->container_number,
|
||||
'container_date' => $container->container_date,
|
||||
'status' => $container->status,
|
||||
'invoice_id' => $invoice->id,
|
||||
'items' => $invoice->items
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function orderShipment($order_id)
|
||||
{
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
// public function orderShipment($order_id)
|
||||
// {
|
||||
// $user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
// Get order
|
||||
$order = $user->orders()->where('order_id', $order_id)->first();
|
||||
// // Get order
|
||||
// $order = $user->orders()->where('order_id', $order_id)->first();
|
||||
|
||||
if (!$order) {
|
||||
return response()->json(['success' => false, 'message' => 'Order not found'], 404);
|
||||
}
|
||||
// if (!$order) {
|
||||
// return response()->json(['success' => false, 'message' => 'Order not found'], 404);
|
||||
// }
|
||||
|
||||
// Find shipment only for this order
|
||||
$shipment = $order->shipments()
|
||||
->with(['items' => function ($q) use ($order) {
|
||||
$q->where('order_id', $order->id);
|
||||
}])
|
||||
->first();
|
||||
// // Find shipment only for this order
|
||||
// $shipment = $order->shipments()
|
||||
// ->with(['items' => function ($q) use ($order) {
|
||||
// $q->where('order_id', $order->id);
|
||||
// }])
|
||||
// ->first();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'shipment' => $shipment
|
||||
]);
|
||||
}
|
||||
// return response()->json([
|
||||
// 'success' => true,
|
||||
// 'shipment' => $shipment
|
||||
// ]);
|
||||
// }
|
||||
|
||||
|
||||
public function orderInvoice($order_id)
|
||||
@@ -179,23 +224,35 @@ public function trackOrder($order_id)
|
||||
{
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
$order = $user->orders()
|
||||
->with('shipments')
|
||||
->where('order_id', $order_id)
|
||||
->first();
|
||||
|
||||
if (!$order) {
|
||||
return response()->json(['success' => false, 'message' => 'Order not found'], 404);
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 401);
|
||||
}
|
||||
|
||||
$shipment = $order->shipments()->first();
|
||||
// Ensure the container belongs to this customer via invoice
|
||||
$invoice = \App\Models\Invoice::where('customer_id', $user->id)
|
||||
->where('container_id', $order_id)
|
||||
->with('container')
|
||||
->first();
|
||||
|
||||
if (!$invoice || !$invoice->container) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Order not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$container = $invoice->container;
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'track' => [
|
||||
'order_id' => $order->order_id,
|
||||
'shipment_status' => $shipment->status ?? 'pending',
|
||||
'shipment_date' => $shipment->shipment_date ?? null,
|
||||
'order_id' => $container->id,
|
||||
'container_number' => $container->container_number,
|
||||
'status' => $container->status,
|
||||
'container_date' => $container->container_date,
|
||||
]
|
||||
]);
|
||||
}
|
||||
@@ -289,44 +346,44 @@ public function invoiceDetails($invoice_id)
|
||||
]);
|
||||
}
|
||||
|
||||
public function confirmOrder($order_id)
|
||||
{
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
// public function confirmOrder($order_id)
|
||||
// {
|
||||
// $user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
if (! $user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 401);
|
||||
}
|
||||
// if (! $user) {
|
||||
// return response()->json([
|
||||
// 'success' => false,
|
||||
// 'message' => 'Unauthorized'
|
||||
// ], 401);
|
||||
// }
|
||||
|
||||
$order = $user->orders()
|
||||
->where('order_id', $order_id)
|
||||
->first();
|
||||
// $order = $user->orders()
|
||||
// ->where('order_id', $order_id)
|
||||
// ->first();
|
||||
|
||||
if (! $order) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Order not found'
|
||||
], 404);
|
||||
}
|
||||
// if (! $order) {
|
||||
// return response()->json([
|
||||
// 'success' => false,
|
||||
// 'message' => 'Order not found'
|
||||
// ], 404);
|
||||
// }
|
||||
|
||||
// 🚫 Only allow confirm from order_placed
|
||||
if ($order->status !== 'order_placed') {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Order cannot be confirmed'
|
||||
], 422);
|
||||
}
|
||||
// // 🚫 Only allow confirm from order_placed
|
||||
// if ($order->status !== 'order_placed') {
|
||||
// return response()->json([
|
||||
// 'success' => false,
|
||||
// 'message' => 'Order cannot be confirmed'
|
||||
// ], 422);
|
||||
// }
|
||||
|
||||
$order->status = 'order_confirmed';
|
||||
$order->save();
|
||||
// $order->status = 'order_confirmed';
|
||||
// $order->save();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Order confirmed successfully'
|
||||
]);
|
||||
}
|
||||
// return response()->json([
|
||||
// 'success' => true,
|
||||
// 'message' => 'Order confirmed successfully'
|
||||
// ]);
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -49,10 +49,10 @@ class Invoice extends Model
|
||||
return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC');
|
||||
}
|
||||
|
||||
public function container()
|
||||
{
|
||||
return $this->belongsTo(Container::class);
|
||||
}
|
||||
// public function container()
|
||||
// {
|
||||
// return $this->belongsTo(Container::class);
|
||||
// }
|
||||
|
||||
public function customer()
|
||||
{
|
||||
@@ -119,4 +119,15 @@ class Invoice extends Model
|
||||
|
||||
return max(0, $grand - $paid);
|
||||
}
|
||||
|
||||
public function isLockedForEdit(): bool
|
||||
{
|
||||
return $this->status === 'paid';
|
||||
}
|
||||
|
||||
public function container()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Container::class, 'container_id');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -89,10 +89,7 @@ class User extends Authenticatable implements JWTSubject
|
||||
{
|
||||
return [];
|
||||
}
|
||||
public function invoices()
|
||||
{
|
||||
return $this->hasMany(\App\Models\Invoice::class, 'customer_id', 'customer_id');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// App\Models\User.php
|
||||
@@ -108,6 +105,10 @@ public function invoiceInstallments()
|
||||
);
|
||||
}
|
||||
|
||||
public function invoices()
|
||||
{
|
||||
return $this->hasMany(\App\Models\Invoice::class, 'customer_id', 'id');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -9,15 +9,29 @@ class AddChargeColumnsToInvoicesTable extends Migration
|
||||
public function up()
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
$table->decimal('charge_groups_total', 15, 2)->nullable()->after('final_amount_with_gst');
|
||||
$table->decimal('grand_total_with_charges', 15, 2)->nullable()->after('charge_groups_total');
|
||||
if (!Schema::hasColumn('invoices', 'charge_groups_total')) {
|
||||
$table->decimal('charge_groups_total', 15, 2)
|
||||
->nullable()
|
||||
->after('final_amount_with_gst');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('invoices', 'grand_total_with_charges')) {
|
||||
$table->decimal('grand_total_with_charges', 15, 2)
|
||||
->nullable()
|
||||
->after('charge_groups_total');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
$table->dropColumn(['charge_groups_total', 'grand_total_with_charges']);
|
||||
if (Schema::hasColumn('invoices', 'charge_groups_total')) {
|
||||
$table->dropColumn('charge_groups_total');
|
||||
}
|
||||
if (Schema::hasColumn('invoices', 'grand_total_with_charges')) {
|
||||
$table->dropColumn('grand_total_with_charges');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
DB::statement("
|
||||
ALTER TABLE `invoices`
|
||||
MODIFY `status` ENUM('pending','paying','paid','overdue')
|
||||
NOT NULL DEFAULT 'pending'
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::statement("
|
||||
ALTER TABLE `invoices`
|
||||
MODIFY `status` ENUM('pending','paid','overdue')
|
||||
NOT NULL DEFAULT 'pending'
|
||||
");
|
||||
}
|
||||
};
|
||||
BIN
public/invoices/invoice-INV-2026-000012.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000012.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000013.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000013.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000014.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000014.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000015.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000015.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000202.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000202.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000205.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000205.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000206.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000206.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000209.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000209.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000213.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000213.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000214.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000214.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000217.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000217.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000218.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000218.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000220.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000220.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000221.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000221.pdf
Normal file
Binary file not shown.
@@ -672,11 +672,19 @@
|
||||
);
|
||||
|
||||
$isTotalColumn = $isTotalQty || $isTotalCbm || $isTotalKg || $isAmount;
|
||||
// row index = headerRowIndex + 1 + offset — ContainerRow मध्ये row_index save आहे
|
||||
$isLockedByInvoice = in_array($row->row_index, $lockedRowIndexes ?? []);
|
||||
$isReadOnly = $isTotalColumn || $container->status !== 'pending' || $isLockedByInvoice;
|
||||
@endphp
|
||||
|
||||
@if($loop->first && $isLockedByInvoice)
|
||||
{{-- पहिल्या cell मध्ये lock icon --}}
|
||||
@endif
|
||||
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="cm-cell-input {{ $isTotalColumn ? 'cm-cell-readonly' : '' }}"
|
||||
class="cm-cell-input {{ $isReadOnly ? 'cm-cell-readonly' : '' }}"
|
||||
name="rows[{{ $row->id }}][{{ $heading }}]"
|
||||
value="{{ $value }}"
|
||||
data-row-id="{{ $row->id }}"
|
||||
@@ -690,10 +698,11 @@
|
||||
data-ttlkg="{{ $isTotalKg ? '1' : '0' }}"
|
||||
data-price="{{ $isPrice ? '1' : '0' }}"
|
||||
data-amount="{{ $isAmount ? '1' : '0' }}"
|
||||
@if($isTotalColumn) readonly @endif
|
||||
{{ $isReadOnly ? 'readonly' : '' }}
|
||||
>
|
||||
</td>
|
||||
@endforeach
|
||||
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
@@ -706,16 +715,18 @@
|
||||
</div>
|
||||
|
||||
@if(!$container->rows->isEmpty())
|
||||
<button id="cmSaveBtnFloating" class="cm-save-btn-floating">
|
||||
<button
|
||||
id="cmSaveBtnFloating"
|
||||
class="cm-save-btn-floating {{ $container->status !== 'pending' ? 'cm-disabled' : '' }}"
|
||||
{{ $container->status !== 'pending' ? 'disabled' : '' }}
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
<div id="cmToast" class="cm-toast">
|
||||
Changes saved successfully.
|
||||
</div>
|
||||
</button>
|
||||
@endif
|
||||
|
||||
|
||||
<script>
|
||||
function cmFilterRows() {
|
||||
function cmFilterRows() {
|
||||
const input = document.getElementById('cmRowSearch');
|
||||
if (!input) return;
|
||||
const filter = input.value.toLowerCase();
|
||||
@@ -734,9 +745,9 @@
|
||||
}
|
||||
rows[i].style.display = match ? '' : 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const form = document.getElementById('cm-edit-rows-form');
|
||||
const btn = document.getElementById('cmSaveBtnFloating');
|
||||
const toast = document.getElementById('cmToast');
|
||||
@@ -843,6 +854,7 @@
|
||||
const target = e.target;
|
||||
if (!target.classList.contains('cm-cell-input')) return;
|
||||
|
||||
// readonly / non-pending cells साठी block
|
||||
if (target.classList.contains('cm-cell-readonly') || target.hasAttribute('readonly')) {
|
||||
target.blur();
|
||||
return;
|
||||
@@ -857,6 +869,11 @@
|
||||
|
||||
if (form && btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
// जर बटण आधीच disabled असेल (non-pending status किंवा processing)
|
||||
if (btn.classList.contains('cm-disabled') || btn.hasAttribute('disabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
btn.classList.add('cm-disabled');
|
||||
const formData = new FormData(form);
|
||||
|
||||
@@ -886,6 +903,7 @@
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
|
||||
@@ -170,6 +170,13 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-select option[value="paying"] {
|
||||
background-color: #e0e7ff;
|
||||
color: #4338ca;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
.filter-select option[value="all"] {
|
||||
background-color: white;
|
||||
color: #1f2937;
|
||||
@@ -534,6 +541,10 @@
|
||||
background: url('/images/status-bg-overdue.png') !important;
|
||||
}
|
||||
|
||||
.badge-paying {
|
||||
background: url('/images/status-bg-paying.png') !important;
|
||||
}
|
||||
|
||||
/* Fallback colors if images don't load - ALL WITH SAME SIZE */
|
||||
.badge.badge-paid {
|
||||
background: linear-gradient(135deg, #d1fae5, #a7f3d0) !important;
|
||||
@@ -554,6 +565,13 @@
|
||||
border-color: #8b5cf6 !important;
|
||||
}
|
||||
|
||||
.badge.badge-paying {
|
||||
background: linear-gradient(135deg, #e0e7ff, #c7d2fe) !important;
|
||||
color: #4338ca !important;
|
||||
border-color: #6366f1 !important;
|
||||
}
|
||||
|
||||
|
||||
/* Entry Button - Centered */
|
||||
.btn-entry {
|
||||
background: linear-gradient(135deg, #3492f8 55%, #1256cc 110%);
|
||||
@@ -1282,11 +1300,14 @@
|
||||
<i class="bi bi-check-circle-fill status-icon"></i>
|
||||
@elseif($invoice->status == 'pending')
|
||||
<i class="bi bi-clock-fill status-icon"></i>
|
||||
@elseif($invoice->status == 'paying')
|
||||
<i class="bi bi-arrow-repeat status-icon"></i> {{-- processing icon --}}
|
||||
@elseif($invoice->status == 'overdue')
|
||||
<i class="bi bi-exclamation-triangle-fill status-icon"></i>
|
||||
@endif
|
||||
{{ ucfirst($invoice->status) }}
|
||||
</span>
|
||||
|
||||
</td>
|
||||
|
||||
<td class="date-cell">
|
||||
@@ -1338,11 +1359,14 @@
|
||||
<i class="bi bi-check-circle-fill status-icon"></i>
|
||||
@elseif($invoice->status == 'pending')
|
||||
<i class="bi bi-clock-fill status-icon"></i>
|
||||
@elseif($invoice->status == 'paying')
|
||||
<i class="bi bi-arrow-repeat status-icon"></i>
|
||||
@elseif($invoice->status == 'overdue')
|
||||
<i class="bi bi-exclamation-triangle-fill status-icon"></i>
|
||||
@endif
|
||||
{{ ucfirst($invoice->status) }}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mobile-invoice-details">
|
||||
@@ -1677,6 +1701,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
${invoice.status === 'paid' ? '<i class="bi bi-check-circle-fill status-icon"></i>' : ''}
|
||||
${invoice.status === 'pending' ? '<i class="bi bi-clock-fill status-icon"></i>' : ''}
|
||||
${invoice.status === 'overdue' ? '<i class="bi bi-exclamation-triangle-fill status-icon"></i>' : ''}
|
||||
${invoice.status === 'paying' ? '<i class="bi bi-arrow-repeat status-icon"></i>' : ''}
|
||||
${invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
|
||||
</span>
|
||||
</td>
|
||||
@@ -1710,6 +1735,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
${invoice.status === 'paid' ? '<i class="bi bi-check-circle-fill status-icon"></i>' : ''}
|
||||
${invoice.status === 'pending' ? '<i class="bi bi-clock-fill status-icon"></i>' : ''}
|
||||
${invoice.status === 'overdue' ? '<i class="bi bi-exclamation-triangle-fill status-icon"></i>' : ''}
|
||||
${invoice.status === 'paying' ? '<i class="bi bi-arrow-repeat status-icon"></i>' : ''}
|
||||
${invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -381,9 +381,11 @@
|
||||
</label>
|
||||
<select name="status" class="form-select-compact" required>
|
||||
<option value="pending" {{ old('status', $invoice->status) === 'pending' ? 'selected' : '' }}>Pending</option>
|
||||
<option value="paying" {{ old('status', $invoice->status) === 'paying' ? 'selected' : '' }}>Paying</option>
|
||||
<option value="paid" {{ old('status', $invoice->status) === 'paid' ? 'selected' : '' }}>Paid</option>
|
||||
<option value="overdue" {{ old('status', $invoice->status) === 'overdue' ? 'selected' : '' }}>Overdue</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Notes --}}
|
||||
|
||||
@@ -640,6 +640,24 @@
|
||||
|
||||
/* ── DIVIDER ── */
|
||||
.section-divider { height: 1px; background: var(--border); margin: 0 2.5rem; }
|
||||
|
||||
.pagination .page-link {
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 4px 10px;
|
||||
font-size: 0.78rem;
|
||||
color: #111827;
|
||||
}
|
||||
.pagination .page-item.active .page-link {
|
||||
background-color: #111827;
|
||||
border-color: #111827;
|
||||
color: #ffffff;
|
||||
}
|
||||
.pagination .page-item.disabled .page-link {
|
||||
background-color: #f3f4f6;
|
||||
color: #9ca3af;
|
||||
border-color: #e5e7eb;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -759,12 +777,17 @@
|
||||
<div class="customer-detail"><strong>Company:</strong> {{ $invoice->company_name }}</div>
|
||||
@endif
|
||||
<div class="customer-detail"><strong>Mobile:</strong> {{ $invoice->customer_mobile }}</div>
|
||||
<div class="customer-detail"><strong>Email:</strong> {{ $invoice->customer_email }}</div>
|
||||
<div class="customer-detail"><strong>Email:</strong>
|
||||
{{ $invoice->customer_email ?: ($invoice->customer->email ?? '-') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="customer-detail"><strong>Address:</strong></div>
|
||||
<div class="customer-detail">{{ $invoice->customer_address }}</div>
|
||||
<div class="customer-detail"><strong>Pincode:</strong> {{ $invoice->pincode }}</div>
|
||||
<div class="customer-detail">
|
||||
{{ $invoice->customer_address ?: ($invoice->customer->address ?? '-') }}
|
||||
</div>
|
||||
<div class="customer-detail"><strong>Pincode:</strong>
|
||||
{{ $invoice->pincode ?: ($invoice->customer->pincode ?? '-') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -780,6 +803,7 @@
|
||||
<i class="fas fa-list"></i> Invoice Items
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="table-responsive">
|
||||
<table class="invoice-table items-table">
|
||||
<thead>
|
||||
@@ -802,7 +826,7 @@
|
||||
<th class="text-center" style="min-width:95px;">Shop No</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody id="itemsTableBody">
|
||||
@foreach($invoice->items as $i => $item)
|
||||
@php
|
||||
$alreadyGrouped = in_array($item->id, $groupedItemIds ?? []);
|
||||
@@ -815,8 +839,12 @@
|
||||
value="{{ $item->id }}"
|
||||
{{ $alreadyGrouped ? 'disabled' : '' }}>
|
||||
</td>
|
||||
<td class="text-center" style="font-weight:600;color:var(--text-muted);">{{ $i + 1 }}</td>
|
||||
<td class="desc-col" style="font-weight:600;color:var(--primary);">{{ $item->description }}</td>
|
||||
<td class="text-center" style="font-weight:600;color:var(--text-muted);">
|
||||
{{ $i + 1 }}
|
||||
</td>
|
||||
<td class="desc-col" style="font-weight:600;color:var(--primary);">
|
||||
{{ $item->description }}
|
||||
</td>
|
||||
<td class="text-center">{{ $item->ctn }}</td>
|
||||
<td class="text-center">{{ $item->qty }}</td>
|
||||
<td class="text-center" style="font-weight:700;">{{ $item->ttl_qty }}</td>
|
||||
@@ -836,29 +864,60 @@
|
||||
class="form-control form-control-sm text-end">
|
||||
</td>
|
||||
@else
|
||||
<td class="text-center price-green">₹{{ number_format($item->price, 2) }}</td>
|
||||
<td class="text-center price-blue">₹{{ number_format($item->ttl_amount, 2) }}</td>
|
||||
<td class="text-center price-green">
|
||||
₹{{ number_format($item->price, 2) }}
|
||||
</td>
|
||||
<td class="text-center price-blue">
|
||||
₹{{ number_format($item->ttl_amount, 2) }}
|
||||
</td>
|
||||
@endif
|
||||
|
||||
<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-shop">{{ $item->shop_no }}</span></td>
|
||||
<td class="text-center">
|
||||
<span class="badge-shop">{{ $item->shop_no }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
||||
@if($invoice->items->isEmpty())
|
||||
<tr>
|
||||
<td colspan="15" class="text-center py-4" style="color:var(--text-muted);font-weight:600;">
|
||||
<i class="fas fa-inbox me-2" style="font-size:1.3rem;opacity:.4;"></i><br>No invoice items found.
|
||||
<td colspan="15" class="text-center py-4"
|
||||
style="color:var(--text-muted);font-weight:600;">
|
||||
<i class="fas fa-inbox me-2"
|
||||
style="font-size:1.3rem;opacity:.4;"></i><br>
|
||||
No invoice items found.
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- Items pagination bar --}}
|
||||
<div class="d-flex justify-content-between align-items-center mt-2"
|
||||
id="itemsPaginationBar"
|
||||
style="font-size:0.8rem;">
|
||||
<div>
|
||||
Showing <span id="itemsStart">0</span> to
|
||||
<span id="itemsEnd">0</span> of
|
||||
<span id="itemsTotal">0</span> items
|
||||
</div>
|
||||
|
||||
<nav aria-label="Items pagination">
|
||||
<ul class="pagination mb-0 pagination-sm" id="itemsPages">
|
||||
{{-- JS will render buttons here --}}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ACTION BAR -->
|
||||
<div class="action-bar">
|
||||
<small>Selected items for charge group: <span id="selectedItemsCount">0</span></small>
|
||||
@@ -1666,8 +1725,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
: '<i class="fas fa-eye-slash"></i> Hide';
|
||||
});
|
||||
});
|
||||
|
||||
// simple select all (duplicate राहिला तरी harmless)
|
||||
// "Select All" checkbox logic
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const selectAll = document.getElementById('selectAllItems');
|
||||
const checkboxes = document.querySelectorAll('.item-select-checkbox');
|
||||
@@ -1682,6 +1740,113 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Items table pagination logic edit blade
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// ====== ITEMS TABLE PAGINATION (50 per page, numbered) ======
|
||||
const itemsPerPage = 50;
|
||||
const itemsTbody = document.getElementById('itemsTableBody');
|
||||
const allItemRows = itemsTbody ? Array.from(itemsTbody.querySelectorAll('tr')) : [];
|
||||
const itemsStart = document.getElementById('itemsStart');
|
||||
const itemsEnd = document.getElementById('itemsEnd');
|
||||
const itemsTotal = document.getElementById('itemsTotal');
|
||||
const itemsPages = document.getElementById('itemsPages');
|
||||
const itemsBar = document.getElementById('itemsPaginationBar');
|
||||
|
||||
let itemsCurrentPage = 1;
|
||||
|
||||
if (allItemRows.length) {
|
||||
// data rows
|
||||
const dataRows = allItemRows.filter(row => !row.querySelector('td[colspan]'));
|
||||
const total = dataRows.length;
|
||||
const totalPages = Math.max(1, Math.ceil(total / itemsPerPage));
|
||||
|
||||
if (itemsTotal) itemsTotal.textContent = total;
|
||||
|
||||
function renderItemsPage() {
|
||||
if (!total) {
|
||||
allItemRows.forEach(r => r.style.display = '');
|
||||
if (itemsStart) itemsStart.textContent = 0;
|
||||
if (itemsEnd) itemsEnd.textContent = 0;
|
||||
if (itemsPages) itemsPages.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const startIndex = (itemsCurrentPage - 1) * itemsPerPage;
|
||||
const endIndex = Math.min(startIndex + itemsPerPage, total);
|
||||
|
||||
dataRows.forEach((row, idx) => {
|
||||
row.style.display = (idx >= startIndex && idx < endIndex) ? '' : 'none';
|
||||
});
|
||||
|
||||
// "no items" row hide
|
||||
allItemRows.forEach(row => {
|
||||
const td = row.querySelector('td[colspan]');
|
||||
if (td) row.style.display = 'none';
|
||||
});
|
||||
|
||||
if (itemsStart) itemsStart.textContent = startIndex + 1;
|
||||
if (itemsEnd) itemsEnd.textContent = endIndex;
|
||||
|
||||
// pagination buttons: ‹ 1 2 … ›
|
||||
if (itemsPages) {
|
||||
itemsPages.innerHTML = '';
|
||||
|
||||
// prev
|
||||
const prevLi = document.createElement('li');
|
||||
prevLi.className = 'page-item' + (itemsCurrentPage === 1 ? ' disabled' : '');
|
||||
prevLi.innerHTML = '<button class="page-link" type="button" aria-label="Previous">‹</button>';
|
||||
if (itemsCurrentPage > 1) {
|
||||
prevLi.querySelector('button').addEventListener('click', () => {
|
||||
itemsCurrentPage--;
|
||||
renderItemsPage();
|
||||
});
|
||||
}
|
||||
itemsPages.appendChild(prevLi);
|
||||
|
||||
// page numbers
|
||||
for (let p = 1; p <= totalPages; p++) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'page-item' + (p === itemsCurrentPage ? ' active' : '');
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'page-link';
|
||||
btn.textContent = p;
|
||||
|
||||
if (p !== itemsCurrentPage) {
|
||||
btn.addEventListener('click', () => {
|
||||
itemsCurrentPage = p;
|
||||
renderItemsPage();
|
||||
});
|
||||
}
|
||||
|
||||
li.appendChild(btn);
|
||||
itemsPages.appendChild(li);
|
||||
}
|
||||
|
||||
// next
|
||||
const nextLi = document.createElement('li');
|
||||
nextLi.className = 'page-item' + (itemsCurrentPage === totalPages ? ' disabled' : '');
|
||||
nextLi.innerHTML = '<button class="page-link" type="button" aria-label="Next">›</button>';
|
||||
if (itemsCurrentPage < totalPages) {
|
||||
nextLi.querySelector('button').addEventListener('click', () => {
|
||||
itemsCurrentPage++;
|
||||
renderItemsPage();
|
||||
});
|
||||
}
|
||||
itemsPages.appendChild(nextLi);
|
||||
}
|
||||
}
|
||||
|
||||
renderItemsPage();
|
||||
} else if (itemsBar) {
|
||||
itemsBar.style.display = 'none';
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user