Compare commits

...

4 Commits

Author SHA256 Message Date
Abhishek Mali
9a6ca49ad7 user order controller update for show view data 2026-03-13 20:19:34 +05:30
Abhishek Mali
bf2689e62d container and invoice api done small api like view invoice traking in order section is remaning 2026-03-13 17:25:58 +05:30
Utkarsh Khedkar
c25b468c77 Status Updated paying 2026-03-12 18:20:09 +05:30
Utkarsh Khedkar
5d8a746876 Status Updated 2026-03-12 18:11:43 +05:30
25 changed files with 584 additions and 277 deletions

View File

@@ -130,7 +130,7 @@ class AdminInvoiceController extends Controller
$data = $request->validate([ $data = $request->validate([
'invoice_date' => 'required|date', 'invoice_date' => 'required|date',
'due_date' => 'required|date|after_or_equal:invoice_date', 'due_date' => 'required|date|after_or_equal:invoice_date',
'status' => 'required|in:pending,paid,overdue', 'status' => 'required|in:pending,paying,paid,overdue',
'notes' => 'nullable|string', 'notes' => 'nullable|string',
]); ]);
@@ -149,8 +149,8 @@ class AdminInvoiceController extends Controller
$this->generateInvoicePDF($invoice); $this->generateInvoicePDF($invoice);
return redirect() return redirect()
->route('admin.invoices.index') ->route('admin.invoices.edit', $invoice->id)
->with('success', 'Invoice updated & PDF generated successfully.'); ->with('success', 'Invoice updated & PDF generated successfully.');
} }
// ------------------------------------------------------------- // -------------------------------------------------------------

View File

@@ -11,7 +11,7 @@ use Illuminate\Http\Request;
use Maatwebsite\Excel\Facades\Excel; use Maatwebsite\Excel\Facades\Excel;
use Carbon\Carbon; use Carbon\Carbon;
use Barryvdh\DomPDF\Facade\Pdf; use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\Storage; // <-- added for Excel download use Illuminate\Support\Facades\Storage;
class ContainerController extends Controller class ContainerController extends Controller
{ {
@@ -518,9 +518,12 @@ class ContainerController extends Controller
$firstMark = $rowsForCustomer[0]['mark']; $firstMark = $rowsForCustomer[0]['mark'];
$snap = $markToSnapshot[$firstMark] ?? null; $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 = new Invoice();
$invoice->container_id = $container->id; $invoice->container_id = $container->id;
$invoice->customer_id = $customerId; $invoice->customer_id = $customerUser->id ?? null; // ✅ integer id store करतोय
$invoice->mark_no = $firstMark; $invoice->mark_no = $firstMark;
$invoice->invoice_number = $this->generateInvoiceNumber(); $invoice->invoice_number = $this->generateInvoiceNumber();
@@ -533,28 +536,24 @@ class ContainerController extends Controller
->addDays(10) ->addDays(10)
->format('Y-m-d'); ->format('Y-m-d');
// Customer User model वरून fetch करा (customer_id string आहे जसे CID-2025-000001) // ✅ Snapshot data from MarkList (backward compatibility)
$customerUser = \App\Models\User::where('customer_id', $customerId)->first(); if ($snap) {
$invoice->customer_name = $snap['customer_name'] ?? null;
$invoice->container_id = $container->id; $invoice->company_name = $snap['company_name'] ?? null;
$invoice->customer_id = $customerUser->id ?? null; // ✅ integer id $invoice->customer_mobile = $snap['mobile_no'] ?? null;
$invoice->mark_no = $firstMark; }
if ($snap) { // ✅ User model वरून email, address, pincode घ्या
$invoice->customer_name = $snap['customer_name'] ?? null; if ($customerUser) {
$invoice->company_name = $snap['company_name'] ?? null;
$invoice->customer_mobile = $snap['mobile_no'] ?? null;
}
$invoice->final_amount = 0;
$invoice->gst_percent = 0;
$invoice->gst_amount = 0;
$invoice->final_amount_with_gst = 0;
// ✅ User model वरून email, address, pincode घ्या
$invoice->customer_email = $customerUser->email ?? null; $invoice->customer_email = $customerUser->email ?? null;
$invoice->customer_address = $customerUser->address ?? null; $invoice->customer_address = $customerUser->address ?? null;
$invoice->pincode = $customerUser->pincode ?? null; $invoice->pincode = $customerUser->pincode ?? null;
}
$invoice->final_amount = 0;
$invoice->gst_percent = 0;
$invoice->gst_amount = 0;
$invoice->final_amount_with_gst = 0;
$uniqueMarks = array_unique(array_column($rowsForCustomer, 'mark')); $uniqueMarks = array_unique(array_column($rowsForCustomer, 'mark'));
$invoice->notes = 'Auto-created from Container ' . $container->container_number $invoice->notes = 'Auto-created from Container ' . $container->container_number
@@ -621,7 +620,18 @@ class ContainerController extends Controller
public function show(Container $container) public function show(Container $container)
{ {
$container->load('rows'); $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) public function updateRows(Request $request, Container $container)
@@ -879,73 +889,71 @@ class ContainerController extends Controller
return Storage::download($path, $fileName); return Storage::download($path, $fileName);
} }
public function popupPopup(Container $container) public function popupPopup(Container $container)
{ {
// existing show सारखाच data वापरू // existing show सारखाच data वापरू
$container->load('rows'); $container->load('rows');
// summary आधीपासून index() मध्ये जसा काढतोस तसाच logic reuse // summary आधीपासून index() मध्ये जसा काढतोस तसाच logic reuse
$rows = $container->rows ?? collect(); $rows = $container->rows ?? collect();
$totalCtn = 0; $totalCtn = 0;
$totalQty = 0; $totalQty = 0;
$totalCbm = 0; $totalCbm = 0;
$totalKg = 0; $totalKg = 0;
$ctnKeys = ['CTN', 'CTNS']; $ctnKeys = ['CTN', 'CTNS'];
$qtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY', 'QTY', 'PCS', 'PIECES']; $qtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY', 'QTY', 'PCS', 'PIECES'];
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM']; $cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT']; $kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
$getFirstNumeric = function (array $data, array $possibleKeys) { $getFirstNumeric = function (array $data, array $possibleKeys) {
$normalizedMap = []; $normalizedMap = [];
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
if ($key === null) continue; if ($key === null) continue;
$normKey = strtoupper((string)$key); $normKey = strtoupper((string)$key);
$normKey = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normKey); $normKey = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normKey);
$normalizedMap[$normKey] = $value; $normalizedMap[$normKey] = $value;
} }
foreach ($possibleKeys as $search) { foreach ($possibleKeys as $search) {
$normSearch = strtoupper($search); $normSearch = strtoupper($search);
$normSearch = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normSearch); $normSearch = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normSearch);
foreach ($normalizedMap as $nKey => $value) { foreach ($normalizedMap as $nKey => $value) {
if (strpos($nKey, $normSearch) !== false) { if (strpos($nKey, $normSearch) !== false) {
if (is_numeric($value)) { if (is_numeric($value)) {
return (float)$value; return (float)$value;
} }
if (is_string($value) && is_numeric(trim($value))) { if (is_string($value) && is_numeric(trim($value))) {
return (float)trim($value); return (float)trim($value);
}
} }
} }
} }
return 0;
};
foreach ($rows as $row) {
$data = $row->data ?? [];
if (!is_array($data)) continue;
$totalCtn += $getFirstNumeric($data, $ctnKeys);
$totalQty += $getFirstNumeric($data, $qtyKeys);
$totalCbm += $getFirstNumeric($data, $cbmKeys);
$totalKg += $getFirstNumeric($data, $kgKeys);
} }
return 0;
};
foreach ($rows as $row) { $summary = [
$data = $row->data ?? []; 'total_ctn' => round($totalCtn, 2),
if (!is_array($data)) continue; 'total_qty' => round($totalQty, 2),
'total_cbm' => round($totalCbm, 3),
'total_kg' => round($totalKg, 2),
];
$totalCtn += $getFirstNumeric($data, $ctnKeys); return view('admin.partials.container_popup_readonly', [
$totalQty += $getFirstNumeric($data, $qtyKeys); 'container' => $container,
$totalCbm += $getFirstNumeric($data, $cbmKeys); 'summary' => $summary,
$totalKg += $getFirstNumeric($data, $kgKeys); ]);
} }
$summary = [
'total_ctn' => round($totalCtn, 2),
'total_qty' => round($totalQty, 2),
'total_cbm' => round($totalCbm, 3),
'total_kg' => round($totalKg, 2),
];
return view('admin.partials.container_popup_readonly', [
'container' => $container,
'summary' => $summary,
]);
}
} }

View File

@@ -21,23 +21,33 @@ class UserOrderController extends Controller
} }
// ------------------------------------- // -------------------------------------
// Get all orders // Get customer invoices with containers
// ------------------------------------- // -------------------------------------
$orders = $user->orders()->with('invoice')->get(); $invoices = $user->invoices()->with('container')->get();
// Unique containers for this customer
$containers = $invoices->pluck('container')->filter()->unique('id');
// ------------------------------------- // -------------------------------------
// Counts // Counts based on container status
// ------------------------------------- // -------------------------------------
$totalOrders = $orders->count(); $totalOrders = $containers->count();
$delivered = $orders->where('status', 'delivered')->count();
$inTransit = $orders->where('status', '!=', 'delivered')->count(); $delivered = $containers->where('status', 'delivered')->count();
$active = $totalOrders;
$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) { $totalAmount = $invoices->sum(function ($invoice) {
return $o->invoice->final_amount_with_gst ?? 0; return $invoice->final_amount_with_gst ?? 0;
}); });
// Format total amount in K, L, Cr // Format total amount in K, L, Cr
@@ -45,13 +55,12 @@ class UserOrderController extends Controller
return response()->json([ return response()->json([
'status' => true, 'status' => true,
'summary' => [ 'summary' => [
'active_orders' => $active, 'active_orders' => $active,
'in_transit_orders' => $inTransit, 'in_transit_orders' => $inTransit,
'delivered_orders' => $delivered, 'delivered_orders' => $delivered,
'total_value' => $formattedAmount, // formatted value 'total_value' => $formattedAmount,
'total_raw' => $totalAmount // original value 'total_raw' => $totalAmount
] ]
]); ]);
} }
@@ -90,20 +99,28 @@ class UserOrderController extends Controller
], 401); ], 401);
} }
// Fetch orders for this user // Get invoices with containers for this customer
$orders = $user->orders() $invoices = $user->invoices()
->with(['invoice', 'shipments']) ->with('container')
->orderBy('id', 'desc') ->orderBy('id', 'desc')
->get() ->get();
->map(function ($o) {
return [ // Extract unique containers
'order_id' => $o->order_id, $containers = $invoices->pluck('container')
'status' => $o->status, ->filter()
'amount' => $o->ttl_amount, ->unique('id')
'description'=> "Order from {$o->origin} to {$o->destination}", ->values();
'created_at' => $o->created_at,
]; $orders = $containers->map(function ($container) {
});
return [
'order_id' => $container->id,
'container_number' => $container->container_number,
'status' => $container->status,
'container_date' => $container->container_date,
'created_at' => $container->created_at,
];
});
return response()->json([ return response()->json([
'success' => true, 'success' => true,
@@ -115,45 +132,73 @@ public function orderDetails($order_id)
{ {
$user = JWTAuth::parseToken()->authenticate(); $user = JWTAuth::parseToken()->authenticate();
$order = $user->orders() if (!$user) {
return response()->json([
'success' => false,
'message' => 'Unauthorized'
], 401);
}
// Find container first
$container = \App\Models\Container::find($order_id);
if (!$container) {
return response()->json([
'success' => false,
'message' => 'Container not found'
], 404);
}
// Find invoice belonging to this user for this container
$invoice = \App\Models\Invoice::where('customer_id', $user->id)
->where('container_id', $container->id)
->with(['items']) ->with(['items'])
->where('order_id', $order_id)
->first(); ->first();
if (!$order) { if (!$invoice) {
return response()->json(['success' => false, 'message' => 'Order not found'], 404); return response()->json([
'success' => false,
'message' => 'Order not found for this user'
], 404);
} }
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'order' => $order 'order' => [
'container_id' => $container->id,
'container_number' => $container->container_number,
'container_date' => $container->container_date,
'status' => $container->status,
'invoice_id' => $invoice->id,
'items' => $invoice->items
]
]); ]);
} }
public function orderShipment($order_id) // public function orderShipment($order_id)
{ // {
$user = JWTAuth::parseToken()->authenticate(); // $user = JWTAuth::parseToken()->authenticate();
// Get order // // Get order
$order = $user->orders()->where('order_id', $order_id)->first(); // $order = $user->orders()->where('order_id', $order_id)->first();
if (!$order) { // if (!$order) {
return response()->json(['success' => false, 'message' => 'Order not found'], 404); // return response()->json(['success' => false, 'message' => 'Order not found'], 404);
} // }
// Find shipment only for this order // // Find shipment only for this order
$shipment = $order->shipments() // $shipment = $order->shipments()
->with(['items' => function ($q) use ($order) { // ->with(['items' => function ($q) use ($order) {
$q->where('order_id', $order->id); // $q->where('order_id', $order->id);
}]) // }])
->first(); // ->first();
return response()->json([ // return response()->json([
'success' => true, // 'success' => true,
'shipment' => $shipment // 'shipment' => $shipment
]); // ]);
} // }
public function orderInvoice($order_id) public function orderInvoice($order_id)
@@ -179,23 +224,35 @@ public function trackOrder($order_id)
{ {
$user = JWTAuth::parseToken()->authenticate(); $user = JWTAuth::parseToken()->authenticate();
$order = $user->orders() if (!$user) {
->with('shipments') return response()->json([
->where('order_id', $order_id) 'success' => false,
->first(); 'message' => 'Unauthorized'
], 401);
if (!$order) {
return response()->json(['success' => false, 'message' => 'Order not found'], 404);
} }
$shipment = $order->shipments()->first(); // Ensure the container belongs to this customer via invoice
$invoice = \App\Models\Invoice::where('customer_id', $user->id)
->where('container_id', $order_id)
->with('container')
->first();
if (!$invoice || !$invoice->container) {
return response()->json([
'success' => false,
'message' => 'Order not found'
], 404);
}
$container = $invoice->container;
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'track' => [ 'track' => [
'order_id' => $order->order_id, 'order_id' => $container->id,
'shipment_status' => $shipment->status ?? 'pending', 'container_number' => $container->container_number,
'shipment_date' => $shipment->shipment_date ?? null, 'status' => $container->status,
'container_date' => $container->container_date,
] ]
]); ]);
} }
@@ -289,44 +346,44 @@ public function invoiceDetails($invoice_id)
]); ]);
} }
public function confirmOrder($order_id) // public function confirmOrder($order_id)
{ // {
$user = JWTAuth::parseToken()->authenticate(); // $user = JWTAuth::parseToken()->authenticate();
if (! $user) { // if (! $user) {
return response()->json([ // return response()->json([
'success' => false, // 'success' => false,
'message' => 'Unauthorized' // 'message' => 'Unauthorized'
], 401); // ], 401);
} // }
$order = $user->orders() // $order = $user->orders()
->where('order_id', $order_id) // ->where('order_id', $order_id)
->first(); // ->first();
if (! $order) { // if (! $order) {
return response()->json([ // return response()->json([
'success' => false, // 'success' => false,
'message' => 'Order not found' // 'message' => 'Order not found'
], 404); // ], 404);
} // }
// 🚫 Only allow confirm from order_placed // // 🚫 Only allow confirm from order_placed
if ($order->status !== 'order_placed') { // if ($order->status !== 'order_placed') {
return response()->json([ // return response()->json([
'success' => false, // 'success' => false,
'message' => 'Order cannot be confirmed' // 'message' => 'Order cannot be confirmed'
], 422); // ], 422);
} // }
$order->status = 'order_confirmed'; // $order->status = 'order_confirmed';
$order->save(); // $order->save();
return response()->json([ // return response()->json([
'success' => true, // 'success' => true,
'message' => 'Order confirmed successfully' // 'message' => 'Order confirmed successfully'
]); // ]);
} // }

View File

@@ -49,10 +49,10 @@ class Invoice extends Model
return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC'); return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC');
} }
public function container() // public function container()
{ // {
return $this->belongsTo(Container::class); // return $this->belongsTo(Container::class);
} // }
public function customer() public function customer()
{ {
@@ -119,4 +119,15 @@ class Invoice extends Model
return max(0, $grand - $paid); 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');
}
} }

View File

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

View File

@@ -9,15 +9,29 @@ class AddChargeColumnsToInvoicesTable extends Migration
public function up() public function up()
{ {
Schema::table('invoices', function (Blueprint $table) { Schema::table('invoices', function (Blueprint $table) {
$table->decimal('charge_groups_total', 15, 2)->nullable()->after('final_amount_with_gst'); if (!Schema::hasColumn('invoices', 'charge_groups_total')) {
$table->decimal('grand_total_with_charges', 15, 2)->nullable()->after('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() public function down()
{ {
Schema::table('invoices', function (Blueprint $table) { 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');
}
}); });
} }
} }

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -672,10 +672,15 @@
); );
$isTotalColumn = $isTotalQty || $isTotalCbm || $isTotalKg || $isAmount; $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
// जर container pending नसेल तर सर्व fields readonly @if($loop->first && $isLockedByInvoice)
$isReadOnly = $isTotalColumn || $container->status !== 'pending'; {{-- पहिल्या cell मध्ये lock icon --}}
@endphp @endif
<td> <td>
<input <input
type="text" type="text"

View File

@@ -169,6 +169,13 @@
color: #6b21a8; color: #6b21a8;
font-weight: 500; font-weight: 500;
} }
.filter-select option[value="paying"] {
background-color: #e0e7ff;
color: #4338ca;
font-weight: 500;
}
.filter-select option[value="all"] { .filter-select option[value="all"] {
background-color: white; background-color: white;
@@ -534,6 +541,10 @@
background: url('/images/status-bg-overdue.png') !important; 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 */ /* Fallback colors if images don't load - ALL WITH SAME SIZE */
.badge.badge-paid { .badge.badge-paid {
background: linear-gradient(135deg, #d1fae5, #a7f3d0) !important; background: linear-gradient(135deg, #d1fae5, #a7f3d0) !important;
@@ -554,6 +565,13 @@
border-color: #8b5cf6 !important; border-color: #8b5cf6 !important;
} }
.badge.badge-paying {
background: linear-gradient(135deg, #e0e7ff, #c7d2fe) !important;
color: #4338ca !important;
border-color: #6366f1 !important;
}
/* Entry Button - Centered */ /* Entry Button - Centered */
.btn-entry { .btn-entry {
background: linear-gradient(135deg, #3492f8 55%, #1256cc 110%); background: linear-gradient(135deg, #3492f8 55%, #1256cc 110%);
@@ -1277,16 +1295,19 @@
</td> </td>
<td> <td>
<span class="badge badge-{{ $invoice->status }}"> <span class="badge badge-{{ $invoice->status }}">
@if($invoice->status == 'paid') @if($invoice->status == 'paid')
<i class="bi bi-check-circle-fill status-icon"></i> <i class="bi bi-check-circle-fill status-icon"></i>
@elseif($invoice->status == 'pending') @elseif($invoice->status == 'pending')
<i class="bi bi-clock-fill status-icon"></i> <i class="bi bi-clock-fill status-icon"></i>
@elseif($invoice->status == 'overdue') @elseif($invoice->status == 'paying')
<i class="bi bi-exclamation-triangle-fill status-icon"></i> <i class="bi bi-arrow-repeat status-icon"></i> {{-- processing icon --}}
@endif @elseif($invoice->status == 'overdue')
{{ ucfirst($invoice->status) }} <i class="bi bi-exclamation-triangle-fill status-icon"></i>
</span> @endif
{{ ucfirst($invoice->status) }}
</span>
</td> </td>
<td class="date-cell"> <td class="date-cell">
@@ -1338,11 +1359,14 @@
<i class="bi bi-check-circle-fill status-icon"></i> <i class="bi bi-check-circle-fill status-icon"></i>
@elseif($invoice->status == 'pending') @elseif($invoice->status == 'pending')
<i class="bi bi-clock-fill status-icon"></i> <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') @elseif($invoice->status == 'overdue')
<i class="bi bi-exclamation-triangle-fill status-icon"></i> <i class="bi bi-exclamation-triangle-fill status-icon"></i>
@endif @endif
{{ ucfirst($invoice->status) }} {{ ucfirst($invoice->status) }}
</span> </span>
</div> </div>
<div class="mobile-invoice-details"> <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 === '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 === '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 === '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)} ${invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
</span> </span>
</td> </td>
@@ -1710,6 +1735,7 @@ document.addEventListener('DOMContentLoaded', function() {
${invoice.status === 'paid' ? '<i class="bi bi-check-circle-fill status-icon"></i>' : ''} ${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 === '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 === '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)} ${invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
</span> </span>
</div> </div>

View File

@@ -381,9 +381,11 @@
</label> </label>
<select name="status" class="form-select-compact" required> <select name="status" class="form-select-compact" required>
<option value="pending" {{ old('status', $invoice->status) === 'pending' ? 'selected' : '' }}>Pending</option> <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="paid" {{ old('status', $invoice->status) === 'paid' ? 'selected' : '' }}>Paid</option>
<option value="overdue" {{ old('status', $invoice->status) === 'overdue' ? 'selected' : '' }}>Overdue</option> <option value="overdue" {{ old('status', $invoice->status) === 'overdue' ? 'selected' : '' }}>Overdue</option>
</select> </select>
</div> </div>
{{-- Notes --}} {{-- Notes --}}

View File

@@ -640,6 +640,24 @@
/* ── DIVIDER ── */ /* ── DIVIDER ── */
.section-divider { height: 1px; background: var(--border); margin: 0 2.5rem; } .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> </style>
</head> </head>
<body> <body>
@@ -781,89 +799,125 @@
@endphp @endphp
<div class="panel"> <div class="panel">
<div class="panel-header"> <div class="panel-header">
<i class="fas fa-list"></i> Invoice Items <i class="fas fa-list"></i> Invoice Items
</div>
<div class="panel-body">
<div class="table-responsive">
<table class="invoice-table items-table">
<thead>
<tr>
<th class="text-center" style="width:44px;">
<input type="checkbox" id="selectAllItems">
</th>
<th class="text-center" style="min-width:50px;">#</th>
<th style="min-width:200px;">Description</th>
<th class="text-center" style="min-width:75px;">CTN</th>
<th class="text-center" style="min-width:75px;">QTY</th>
<th class="text-center" style="min-width:95px;">TTL/QTY</th>
<th class="text-center" style="min-width:75px;">Unit</th>
<th class="text-center" style="min-width:110px;">Price</th>
<th class="text-center" style="min-width:125px;">TTL Amount</th>
<th class="text-center" style="min-width:85px;">CBM</th>
<th class="text-center" style="min-width:95px;">TTL CBM</th>
<th class="text-center" style="min-width:80px;">KG</th>
<th class="text-center" style="min-width:95px;">TTL KG</th>
<th class="text-center" style="min-width:95px;">Shop No</th>
</tr>
</thead>
<tbody id="itemsTableBody">
@foreach($invoice->items as $i => $item)
@php
$alreadyGrouped = in_array($item->id, $groupedItemIds ?? []);
@endphp
<tr class="{{ $alreadyGrouped ? 'grouped-item-row' : '' }}">
<td class="text-center">
<input type="checkbox"
class="item-select-checkbox"
name="item_ids[]"
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">{{ $item->ctn }}</td>
<td class="text-center">{{ $item->qty }}</td>
<td class="text-center" style="font-weight:700;">{{ $item->ttl_qty }}</td>
<td class="text-center">{{ $item->unit }}</td>
@if($isEmbedded)
<td class="text-center" style="min-width:120px;">
<input type="number" step="0.01" min="0"
name="items[{{ $item->id }}][price]"
value="{{ old('items.' . $item->id . '.price', $item->price) }}"
class="form-control form-control-sm text-end">
</td>
<td class="text-center" style="min-width:140px;">
<input type="number" step="0.01" min="0"
name="items[{{ $item->id }}][ttl_amount]"
value="{{ old('items.' . $item->id . '.ttl_amount', $item->ttl_amount) }}"
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>
@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>
</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>
</tr>
@endif
</tbody>
</table>
</div> </div>
<div class="table-responsive"> {{-- Items pagination bar --}}
<table class="invoice-table items-table"> <div class="d-flex justify-content-between align-items-center mt-2"
<thead> id="itemsPaginationBar"
<tr> style="font-size:0.8rem;">
<th class="text-center" style="width:44px;"> <div>
<input type="checkbox" id="selectAllItems"> Showing <span id="itemsStart">0</span> to
</th> <span id="itemsEnd">0</span> of
<th class="text-center" style="min-width:50px;">#</th> <span id="itemsTotal">0</span> items
<th style="min-width:200px;">Description</th> </div>
<th class="text-center" style="min-width:75px;">CTN</th>
<th class="text-center" style="min-width:75px;">QTY</th>
<th class="text-center" style="min-width:95px;">TTL/QTY</th>
<th class="text-center" style="min-width:75px;">Unit</th>
<th class="text-center" style="min-width:110px;">Price</th>
<th class="text-center" style="min-width:125px;">TTL Amount</th>
<th class="text-center" style="min-width:85px;">CBM</th>
<th class="text-center" style="min-width:95px;">TTL CBM</th>
<th class="text-center" style="min-width:80px;">KG</th>
<th class="text-center" style="min-width:95px;">TTL KG</th>
<th class="text-center" style="min-width:95px;">Shop No</th>
</tr>
</thead>
<tbody>
@foreach($invoice->items as $i => $item)
@php
$alreadyGrouped = in_array($item->id, $groupedItemIds ?? []);
@endphp
<tr class="{{ $alreadyGrouped ? 'grouped-item-row' : '' }}">
<td class="text-center">
<input type="checkbox"
class="item-select-checkbox"
name="item_ids[]"
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">{{ $item->ctn }}</td>
<td class="text-center">{{ $item->qty }}</td>
<td class="text-center" style="font-weight:700;">{{ $item->ttl_qty }}</td>
<td class="text-center">{{ $item->unit }}</td>
@if($isEmbedded) <nav aria-label="Items pagination">
<td class="text-center" style="min-width:120px;"> <ul class="pagination mb-0 pagination-sm" id="itemsPages">
<input type="number" step="0.01" min="0" {{-- JS will render buttons here --}}
name="items[{{ $item->id }}][price]" </ul>
value="{{ old('items.' . $item->id . '.price', $item->price) }}" </nav>
class="form-control form-control-sm text-end"> </div>
</td>
<td class="text-center" style="min-width:140px;">
<input type="number" step="0.01" min="0"
name="items[{{ $item->id }}][ttl_amount]"
value="{{ old('items.' . $item->id . '.ttl_amount', $item->ttl_amount) }}"
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>
@endif
<td class="text-center">{{ $item->cbm }}</td> </div>
<td class="text-center">{{ $item->ttl_cbm }}</td> </div>
<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>
</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>
</tr>
@endif
</tbody>
</table>
</div> </div>
<!-- ACTION BAR --> <!-- ACTION BAR -->
<div class="action-bar"> <div class="action-bar">
<small>Selected items for charge group: <span id="selectedItemsCount">0</span></small> <small>Selected items for charge group: <span id="selectedItemsCount">0</span></small>
@@ -1671,8 +1725,7 @@ document.addEventListener('DOMContentLoaded', function () {
: '<i class="fas fa-eye-slash"></i> Hide'; : '<i class="fas fa-eye-slash"></i> Hide';
}); });
}); });
// "Select All" checkbox logic
// simple select all (duplicate राहिला तरी harmless)
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const selectAll = document.getElementById('selectAllItems'); const selectAll = document.getElementById('selectAllItems');
const checkboxes = document.querySelectorAll('.item-select-checkbox'); const checkboxes = document.querySelectorAll('.item-select-checkbox');
@@ -1687,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> </script>