Compare commits
43 Commits
main
...
0c51ed1489
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
0c51ed1489 | ||
|
|
9cc6959396 | ||
|
|
c11467068c | ||
|
|
599023166a | ||
|
|
e188780329 | ||
|
|
338425535e | ||
|
|
f7856a6755 | ||
|
|
8f95091673 | ||
|
|
7362ef6bdc | ||
|
|
e872b83ea3 | ||
|
|
6ccf2cf84e | ||
|
|
4637f0b189 | ||
|
|
952dd7eddd | ||
|
|
451be1a533 | ||
|
|
cd9a786ef4 | ||
|
|
a6dd919d3f | ||
|
|
e0a8a5c69c | ||
|
|
7fa03688aa | ||
|
|
1885d3beef | ||
|
|
72a81fa111 | ||
|
|
044bfe5563 | ||
|
|
8ca8f05b93 | ||
|
|
2741129740 | ||
|
|
1bce2be826 | ||
|
|
ea2532efc8 | ||
|
|
ccce02f43e | ||
|
|
cdb6cab57d | ||
|
|
3941b06355 | ||
|
|
d2730e78f6 | ||
|
|
80c6e42e0c | ||
|
|
e455c271c4 | ||
|
|
48f7ab82ff | ||
|
|
c4097ecbde | ||
|
|
8a0d122e2c | ||
|
|
7ef28e06ae | ||
|
|
752f5ee873 | ||
|
|
84bf42f992 | ||
|
|
fc9a401a8c | ||
|
|
3590e8f873 | ||
|
|
f6fb304b7a | ||
|
|
6b41a447bb | ||
|
|
5dc9fc7db4 | ||
|
|
1aad6b231e |
12
.env.example
12
.env.example
@@ -20,12 +20,12 @@ LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=kent_logistics6
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
101
app/Events/NewChatMessage.php
Normal file
101
app/Events/NewChatMessage.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\ChatMessage;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class NewChatMessage implements ShouldBroadcastNow
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public $message;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(ChatMessage $message)
|
||||
{
|
||||
// Also load sender polymorphic relationship
|
||||
$message->load('sender');
|
||||
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* The channel the event should broadcast on.
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return [
|
||||
new PrivateChannel('ticket.' . $this->message->ticket_id),
|
||||
new PrivateChannel('admin.chat') // 👈 ADD THIS
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data sent to frontend (Blade + Flutter)
|
||||
*/
|
||||
public function broadcastWith()
|
||||
{
|
||||
\Log::info('APP_URL USED IN EVENT', [
|
||||
'url' => config('app.url'),
|
||||
]);
|
||||
|
||||
\Log::info("DEBUG: NewChatMessage broadcasting on channel ticket.".$this->message->ticket_id);
|
||||
|
||||
\Log::info("EVENT BROADCAST FIRED", [
|
||||
'channel' => 'ticket.'.$this->message->ticket_id,
|
||||
'sender_type' => $this->message->sender_type,
|
||||
'sender_id' => $this->message->sender_id,
|
||||
]);
|
||||
|
||||
|
||||
return [
|
||||
'id' => $this->message->id,
|
||||
'ticket_id' => $this->message->ticket_id,
|
||||
'sender_id' => $this->message->sender_id,
|
||||
'sender_type' => $this->message->sender_type,
|
||||
'message' => $this->message->message,
|
||||
'client_id' => $this->message->client_id,
|
||||
|
||||
// ✅ relative path only
|
||||
'file_path' => $this->message->file_path ?? null,
|
||||
'file_type' => $this->message->file_type ?? null,
|
||||
|
||||
'sender' => [
|
||||
'id' => $this->message->sender->id,
|
||||
'name' => $this->getSenderName(),
|
||||
'is_admin' => $this->message->sender_type === \App\Models\Admin::class,
|
||||
],
|
||||
|
||||
'created_at' => $this->message->created_at->toDateTimeString(),
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to extract sender name
|
||||
*/
|
||||
private function getSenderName()
|
||||
{
|
||||
$sender = $this->message->sender;
|
||||
|
||||
// User has customer_name (in your app)
|
||||
if ($this->message->sender_type === \App\Models\User::class) {
|
||||
return $sender->customer_name ?? $sender->name ?? "User";
|
||||
}
|
||||
|
||||
// Admin model has ->name
|
||||
return $sender->name ?? "Admin";
|
||||
}
|
||||
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'NewChatMessage';
|
||||
}
|
||||
|
||||
}
|
||||
63
app/Exports/InvoicesExport.php
Normal file
63
app/Exports/InvoicesExport.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Maatwebsite\Excel\Concerns\FromView;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class InvoicesExport implements FromView
|
||||
{
|
||||
protected $request;
|
||||
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function view(): View
|
||||
{
|
||||
$request = $this->request;
|
||||
|
||||
$invoices = DB::table('invoices')
|
||||
->leftJoin('containers', 'containers.id', '=', 'invoices.container_id')
|
||||
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
|
||||
->select(
|
||||
'invoices.invoice_number',
|
||||
'invoices.invoice_date',
|
||||
'invoices.mark_no',
|
||||
'containers.container_number',
|
||||
'containers.container_date',
|
||||
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
|
||||
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name'),
|
||||
'invoices.final_amount',
|
||||
'invoices.final_amount_with_gst',
|
||||
'invoices.status as invoice_status'
|
||||
)
|
||||
->when($request->filled('search'), function ($q) use ($request) {
|
||||
$search = trim($request->search);
|
||||
$q->where(function ($qq) use ($search) {
|
||||
$qq->where('invoices.invoice_number', 'like', "%{$search}%")
|
||||
->orWhere('invoices.mark_no', 'like', "%{$search}%")
|
||||
->orWhere('containers.container_number', 'like', "%{$search}%")
|
||||
->orWhere('mark_list.company_name', 'like', "%{$search}%")
|
||||
->orWhere('mark_list.customer_name', 'like', "%{$search}%");
|
||||
});
|
||||
})
|
||||
->when($request->filled('status'), function ($q) use ($request) {
|
||||
$q->where('invoices.status', $request->status);
|
||||
})
|
||||
->when($request->filled('from_date'), function ($q) use ($request) {
|
||||
$q->whereDate('invoices.invoice_date', '>=', $request->from_date);
|
||||
})
|
||||
->when($request->filled('to_date'), function ($q) use ($request) {
|
||||
$q->whereDate('invoices.invoice_date', '<=', $request->to_date);
|
||||
})
|
||||
->orderByDesc('containers.container_date')
|
||||
->orderByDesc('invoices.id')
|
||||
->get();
|
||||
|
||||
return view('admin.pdf.invoices_excel', compact('invoices'));
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,11 @@ use App\Models\Order;
|
||||
use Illuminate\Http\Request;
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class OrdersExport implements FromCollection, WithHeadings
|
||||
{
|
||||
protected $request;
|
||||
protected Request $request;
|
||||
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
@@ -18,61 +19,99 @@ class OrdersExport implements FromCollection, WithHeadings
|
||||
|
||||
private function buildQuery()
|
||||
{
|
||||
$query = Order::with(['markList', 'invoice', 'shipments']);
|
||||
$query = Order::query()->with([
|
||||
'markList',
|
||||
'invoice',
|
||||
'shipments',
|
||||
]);
|
||||
|
||||
// SEARCH
|
||||
if ($this->request->filled('search')) {
|
||||
$search = $this->request->search;
|
||||
$query->where(function($q) use ($search) {
|
||||
$q->where('order_id', 'like', "%{$search}%")
|
||||
->orWhereHas('markList', function($q2) use ($search) {
|
||||
$search = trim($this->request->search);
|
||||
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('orders.order_id', 'like', "%{$search}%")
|
||||
->orWhereHas('markList', function ($q2) use ($search) {
|
||||
$q2->where('company_name', 'like', "%{$search}%")
|
||||
->orWhere('customer_id', 'like', "%{$search}%");
|
||||
->orWhere('customer_id', 'like', "%{$search}%")
|
||||
->orWhere('origin', 'like', "%{$search}%")
|
||||
->orWhere('destination', 'like', "%{$search}%");
|
||||
})
|
||||
->orWhereHas('invoice', function($q3) use ($search) {
|
||||
->orWhereHas('invoice', function ($q3) use ($search) {
|
||||
$q3->where('invoice_number', 'like', "%{$search}%");
|
||||
})
|
||||
->orWhereHas('shipments', function ($q4) use ($search) {
|
||||
// ✅ FIXED
|
||||
$q4->where('shipments.shipment_id', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// INVOICE STATUS
|
||||
// INVOICE STATUS (FIXED)
|
||||
if ($this->request->filled('status')) {
|
||||
$query->whereHas('invoice', function($q) {
|
||||
$q->where('status', $this->request->status);
|
||||
$query->where(function ($q) {
|
||||
$q->whereHas('invoice', function ($q2) {
|
||||
$q2->where('status', $this->request->status);
|
||||
})
|
||||
->orWhereDoesntHave('invoice');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// SHIPMENT STATUS (FIXED)
|
||||
if ($this->request->filled('shipment')) {
|
||||
$query->whereHas('shipments', function($q) {
|
||||
$q->where('status', $this->request->shipment);
|
||||
$query->where(function ($q) {
|
||||
$q->whereHas('shipments', function ($q2) {
|
||||
$q2->where('status', $this->request->shipment);
|
||||
})
|
||||
->orWhereDoesntHave('shipments');
|
||||
});
|
||||
}
|
||||
|
||||
return $query->latest('id');
|
||||
|
||||
// DATE RANGE
|
||||
if ($this->request->filled('from_date')) {
|
||||
$query->whereDate('orders.created_at', '>=', $this->request->from_date);
|
||||
}
|
||||
|
||||
if ($this->request->filled('to_date')) {
|
||||
$query->whereDate('orders.created_at', '<=', $this->request->to_date);
|
||||
}
|
||||
|
||||
return $query->latest('orders.id');
|
||||
}
|
||||
|
||||
public function collection()
|
||||
{
|
||||
$orders = $this->buildQuery()->get();
|
||||
return $this->buildQuery()->get()->map(function ($order) {
|
||||
|
||||
// Map to simple array rows suitable for Excel
|
||||
return $orders->map(function($order) {
|
||||
$mark = $order->markList;
|
||||
$invoice = $order->invoice;
|
||||
$shipment = $order->shipments->first() ?? null;
|
||||
$mark = $order->markList;
|
||||
$invoice = $order->invoice;
|
||||
$shipment = $order->shipments->first();
|
||||
|
||||
return [
|
||||
'Order ID' => $order->order_id,
|
||||
'Shipment ID' => $shipment->shipment_id ?? '-',
|
||||
'Customer ID' => $mark->customer_id ?? '-',
|
||||
'Company' => $mark->company_name ?? '-',
|
||||
'Origin' => $mark->origin ?? $order->origin ?? '-',
|
||||
'Destination' => $mark->destination ?? $order->destination ?? '-',
|
||||
'Order Date' => $order->created_at ? $order->created_at->format('d-m-Y') : '-',
|
||||
'Invoice No' => $invoice->invoice_number ?? '-',
|
||||
'Invoice Date' => $invoice?->invoice_date ? \Carbon\Carbon::parse($invoice->invoice_date)->format('d-m-Y') : '-',
|
||||
'Amount' => $invoice?->final_amount ? number_format($invoice->final_amount, 2) : '-',
|
||||
'Amount + GST' => $invoice?->final_amount_with_gst ? number_format($invoice->final_amount_with_gst, 2) : '-',
|
||||
'Invoice Status' => $invoice->status ? ucfirst($invoice->status) : 'Pending',
|
||||
'Shipment Status' => $shipment?->status ? ucfirst(str_replace('_', ' ', $shipment->status)) : 'Pending',
|
||||
'Order ID' => $order->order_id ?? '-',
|
||||
'Shipment ID' => $shipment?->shipment_id ?? '-',
|
||||
'Customer ID' => $mark?->customer_id ?? '-',
|
||||
'Company' => $mark?->company_name ?? '-',
|
||||
'Origin' => $mark?->origin ?? $order->origin ?? '-',
|
||||
'Destination' => $mark?->destination ?? $order->destination ?? '-',
|
||||
'Order Date' => $order->created_at
|
||||
? $order->created_at->format('d-m-Y')
|
||||
: '-',
|
||||
'Invoice No' => $invoice?->invoice_number ?? '-',
|
||||
'Invoice Date' => $invoice?->invoice_date
|
||||
? Carbon::parse($invoice->invoice_date)->format('d-m-Y')
|
||||
: '-',
|
||||
'Amount' => $invoice?->final_amount !== null
|
||||
? number_format($invoice->final_amount, 2)
|
||||
: '0.00',
|
||||
'Amount + GST' => $invoice?->final_amount_with_gst !== null
|
||||
? number_format($invoice->final_amount_with_gst, 2)
|
||||
: '0.00',
|
||||
'Invoice Status' => ucfirst($invoice?->status ?? 'pending'),
|
||||
'Shipment Status' => ucfirst(str_replace('_', ' ', $shipment?->status ?? 'pending')),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
105
app/Http/Controllers/Admin/AdminChatController.php
Normal file
105
app/Http/Controllers/Admin/AdminChatController.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\SupportTicket;
|
||||
use App\Models\ChatMessage;
|
||||
use App\Events\NewChatMessage;
|
||||
|
||||
class AdminChatController extends Controller
|
||||
{
|
||||
/**
|
||||
* Page 1: List all active user chats
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$tickets = SupportTicket::with('user')
|
||||
->withCount([
|
||||
'messages as unread_count' => function ($q) {
|
||||
$q->where('sender_type', \App\Models\User::class)
|
||||
->where('read_by_admin', false);
|
||||
}
|
||||
])
|
||||
->orderBy('updated_at', 'desc')
|
||||
->get();
|
||||
|
||||
return view('admin.chat_support', compact('tickets'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Page 2: Open chat window for a specific user
|
||||
*/
|
||||
public function openChat($ticketId)
|
||||
{
|
||||
$ticket = SupportTicket::with('user')->findOrFail($ticketId);
|
||||
|
||||
// ✅ MARK USER MESSAGES AS READ FOR ADMIN
|
||||
ChatMessage::where('ticket_id', $ticketId)
|
||||
->where('sender_type', \App\Models\User::class)
|
||||
->where('read_by_admin', false)
|
||||
->update(['read_by_admin' => true]);
|
||||
|
||||
$messages = ChatMessage::where('ticket_id', $ticketId)
|
||||
->orderBy('created_at', 'asc')
|
||||
->with('sender')
|
||||
->get();
|
||||
|
||||
return view('admin.chat_window', compact('ticket', 'messages'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Admin sends a message to the user
|
||||
*/
|
||||
public function sendMessage(Request $request, $ticketId)
|
||||
{
|
||||
$request->validate([
|
||||
'message' => 'nullable|string',
|
||||
'file' => 'nullable|file|max:20480', // 20 MB
|
||||
]);
|
||||
|
||||
$ticket = SupportTicket::findOrFail($ticketId);
|
||||
$admin = auth('admin')->user();
|
||||
|
||||
$data = [
|
||||
'ticket_id' => $ticketId,
|
||||
'sender_id' => $admin->id,
|
||||
'sender_type' => \App\Models\Admin::class,
|
||||
'message' => $request->message,
|
||||
|
||||
'read_by_admin' => true,
|
||||
'read_by_user' => false,
|
||||
];
|
||||
|
||||
// File Upload
|
||||
if ($request->hasFile('file')) {
|
||||
$path = $request->file('file')->store('chat', 'public');
|
||||
$data['file_path'] = $path;
|
||||
$data['file_type'] = $request->file('file')->getMimeType();
|
||||
}
|
||||
|
||||
// Save message
|
||||
$message = ChatMessage::create($data);
|
||||
$message->load('sender');
|
||||
|
||||
\Log::info("DEBUG: ChatController sendMessage called", [
|
||||
'ticket_id' => $ticketId,
|
||||
'payload' => $request->all()
|
||||
]);
|
||||
|
||||
// Broadcast real-time
|
||||
broadcast(new NewChatMessage($message));
|
||||
|
||||
\Log::info("DEBUG: ChatController sendMessage called 79", [
|
||||
'ticket_id' => $ticketId,
|
||||
'payload' => $request->all()
|
||||
]);
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -19,32 +19,37 @@ class AdminCustomerController extends Controller
|
||||
$search = $request->search;
|
||||
$status = $request->status;
|
||||
|
||||
$query = User::with(['marks', 'orders'])->orderBy('id', 'desc');
|
||||
$query = User::with([
|
||||
'marks',
|
||||
'orders',
|
||||
'invoices.installments' // 🔥 IMPORTANT
|
||||
])->orderBy('id', 'desc');
|
||||
|
||||
// SEARCH FILTER
|
||||
if (!empty($search)) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('customer_name', 'like', "%$search%")
|
||||
->orWhere('email', 'like', "%$search%")
|
||||
->orWhere('mobile_no', 'like', "%$search%")
|
||||
->orWhere('customer_id', 'like', "%$search%");
|
||||
->orWhere('email', 'like', "%$search%")
|
||||
->orWhere('mobile_no', 'like', "%$search%")
|
||||
->orWhere('customer_id', 'like', "%$search%");
|
||||
});
|
||||
}
|
||||
|
||||
// STATUS FILTER
|
||||
if (!empty($status) && in_array($status, ['active', 'inactive'])) {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
// Get all customers for statistics (without pagination)
|
||||
$allCustomers = $query->get();
|
||||
|
||||
// Get paginated customers for the table (10 per page)
|
||||
$customers = $query->paginate(10);
|
||||
|
||||
return view('admin.customers', compact('customers', 'allCustomers', 'search', 'status'));
|
||||
return view('admin.customers', compact(
|
||||
'customers',
|
||||
'allCustomers',
|
||||
'search',
|
||||
'status'
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// SHOW ADD CUSTOMER FORM
|
||||
// ---------------------------------------------------------
|
||||
@@ -106,20 +111,36 @@ class AdminCustomerController extends Controller
|
||||
// VIEW CUSTOMER FULL DETAILS
|
||||
// ---------------------------------------------------------
|
||||
public function view($id)
|
||||
{
|
||||
$customer = User::with(['marks', 'orders'])->findOrFail($id);
|
||||
{
|
||||
$customer = User::with([
|
||||
'marks',
|
||||
'orders',
|
||||
'invoices.installments'
|
||||
])->findOrFail($id);
|
||||
|
||||
$totalOrders = $customer->orders->count();
|
||||
$totalAmount = $customer->orders->sum('ttl_amount');
|
||||
$recentOrders = $customer->orders()->latest()->take(5)->get();
|
||||
// Orders
|
||||
$totalOrders = $customer->orders->count();
|
||||
$totalOrderAmount = $customer->orders->sum('ttl_amount');
|
||||
|
||||
// Invoices (PAYABLE)
|
||||
$totalPayable = $customer->invoices->sum('final_amount_with_gst');
|
||||
|
||||
// Paid via installments
|
||||
$totalPaid = $customer->invoiceInstallments->sum('amount');
|
||||
|
||||
// Remaining
|
||||
$totalRemaining = max($totalPayable - $totalPaid, 0);
|
||||
|
||||
return view('admin.customers_view', compact(
|
||||
'customer',
|
||||
'totalOrders',
|
||||
'totalOrderAmount',
|
||||
'totalPayable',
|
||||
'totalPaid',
|
||||
'totalRemaining'
|
||||
));
|
||||
}
|
||||
|
||||
return view('admin.customers_view', compact(
|
||||
'customer',
|
||||
'totalOrders',
|
||||
'totalAmount',
|
||||
'recentOrders'
|
||||
));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// TOGGLE STATUS ACTIVE / INACTIVE
|
||||
|
||||
@@ -3,38 +3,79 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use Mpdf\Mpdf;
|
||||
use App\Models\InvoiceInstallment;
|
||||
use App\Models\InvoiceChargeGroup;
|
||||
use App\Models\InvoiceChargeGroupItem;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Mpdf\Mpdf;
|
||||
|
||||
class AdminInvoiceController extends Controller
|
||||
{
|
||||
// -------------------------------------------------------------
|
||||
// INVOICE LIST PAGE
|
||||
// -------------------------------------------------------------
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
$invoices = Invoice::with(['order.shipments'])->latest()->get();
|
||||
// Container relation सह invoices load करतो
|
||||
$query = Invoice::with(['items', 'customer', 'container']);
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('invoice_number', 'like', "%{$search}%")
|
||||
->orWhere('customer_name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('status') && $request->status !== 'all') {
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
if ($request->filled('start_date')) {
|
||||
$query->whereDate('invoice_date', '>=', $request->start_date);
|
||||
}
|
||||
|
||||
if ($request->filled('end_date')) {
|
||||
$query->whereDate('invoice_date', '<=', $request->end_date);
|
||||
}
|
||||
|
||||
$invoices = $query->latest()->get();
|
||||
|
||||
return view('admin.invoice', compact('invoices'));
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// POPUP VIEW (AJAX)
|
||||
// POPUP VIEW + CUSTOMER DATA SYNC
|
||||
// -------------------------------------------------------------
|
||||
public function popup($id)
|
||||
{
|
||||
$invoice = Invoice::with(['items', 'order'])->findOrFail($id);
|
||||
$invoice = Invoice::with([
|
||||
'items',
|
||||
'chargeGroups.items',
|
||||
])->findOrFail($id);
|
||||
|
||||
// Find actual Shipment record
|
||||
$shipment = \App\Models\Shipment::whereHas('items', function ($q) use ($invoice) {
|
||||
$q->where('order_id', $invoice->order_id);
|
||||
})
|
||||
->first();
|
||||
// demo update असेल तर ठेव/काढ
|
||||
$invoice->update([
|
||||
'customer_email' => 'test@demo.com',
|
||||
'customer_address' => 'TEST ADDRESS',
|
||||
'pincode' => '999999',
|
||||
]);
|
||||
|
||||
return view('admin.popup_invoice', compact('invoice', 'shipment'));
|
||||
$shipment = null;
|
||||
|
||||
// आधीच group मध्ये असलेले item ids
|
||||
$groupedItemIds = $invoice->chargeGroups
|
||||
->flatMap(fn($group) => $group->items->pluck('invoice_item_id'))
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return view('admin.popup_invoice', compact('invoice', 'shipment', 'groupedItemIds'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
@@ -42,76 +83,102 @@ class AdminInvoiceController extends Controller
|
||||
// -------------------------------------------------------------
|
||||
public function edit($id)
|
||||
{
|
||||
$invoice = Invoice::with(['order.shipments'])->findOrFail($id);
|
||||
$shipment = $invoice->order?->shipments?->first();
|
||||
$invoice = Invoice::with([
|
||||
'items',
|
||||
'customer',
|
||||
'container',
|
||||
'chargeGroups.items',
|
||||
])->findOrFail($id);
|
||||
|
||||
return view('admin.invoice_edit', compact('invoice', 'shipment'));
|
||||
$shipment = null;
|
||||
|
||||
$groupedItemIds = $invoice->chargeGroups
|
||||
->flatMap(function ($group) {
|
||||
return $group->items->pluck('invoice_item_id');
|
||||
})
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return view('admin.invoice_edit', compact('invoice', 'shipment', 'groupedItemIds'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// UPDATE INVOICE
|
||||
// UPDATE INVOICE (HEADER LEVEL)
|
||||
// -------------------------------------------------------------
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
Log::info("🟡 Invoice Update Request Received", [
|
||||
Log::info('🟡 Invoice Update Request Received', [
|
||||
'invoice_id' => $id,
|
||||
'request' => $request->all()
|
||||
'request' => $request->all(),
|
||||
]);
|
||||
|
||||
$invoice = Invoice::findOrFail($id);
|
||||
|
||||
$data = $request->validate([
|
||||
'invoice_date' => 'required|date',
|
||||
'due_date' => 'required|date|after_or_equal:invoice_date',
|
||||
'final_amount' => 'required|numeric|min:0',
|
||||
'tax_type' => 'required|in:gst,igst',
|
||||
'tax_percent' => 'required|numeric|min:0|max:28',
|
||||
'status' => 'required|in:pending,paid,overdue',
|
||||
'notes' => 'nullable|string',
|
||||
'invoice_date' => 'required|date',
|
||||
'due_date' => 'required|date|after_or_equal:invoice_date',
|
||||
'final_amount' => 'required|numeric|min:0',
|
||||
'tax_type' => 'required|in:gst,igst',
|
||||
'tax_percent' => 'required|numeric|min:0|max:28',
|
||||
'status' => 'required|in:pending,paid,overdue',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
|
||||
Log::info("✅ Validated Invoice Update Data", $data);
|
||||
Log::info('✅ Validated Invoice Update Data', $data);
|
||||
|
||||
$finalAmount = floatval($data['final_amount']);
|
||||
$taxPercent = floatval($data['tax_percent']);
|
||||
$taxAmount = 0;
|
||||
$finalAmount = (float) $data['final_amount'];
|
||||
$taxPercent = (float) $data['tax_percent'];
|
||||
|
||||
if ($data['tax_type'] === 'gst') {
|
||||
Log::info("🟢 GST Selected", compact('taxPercent'));
|
||||
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'));
|
||||
Log::info('🔵 IGST Selected', compact('taxPercent'));
|
||||
|
||||
$data['cgst_percent'] = 0;
|
||||
$data['sgst_percent'] = 0;
|
||||
$data['igst_percent'] = $taxPercent;
|
||||
}
|
||||
|
||||
$taxAmount = ($finalAmount * $taxPercent) / 100;
|
||||
$gstAmount = ($finalAmount * $taxPercent) / 100;
|
||||
$data['gst_amount'] = $gstAmount;
|
||||
$data['final_amount_with_gst'] = $finalAmount + $gstAmount;
|
||||
$data['gst_percent'] = $taxPercent;
|
||||
|
||||
$data['gst_amount'] = $taxAmount;
|
||||
$data['final_amount_with_gst'] = $finalAmount + $taxAmount;
|
||||
$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'],
|
||||
Log::info('📌 Final Calculated Invoice Values', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'final_amount' => $finalAmount,
|
||||
'gst_amount' => $data['gst_amount'],
|
||||
'final_amount_with_gst' => $data['final_amount_with_gst'],
|
||||
'tax_type' => $data['tax_type'],
|
||||
'cgst_percent' => $data['cgst_percent'],
|
||||
'sgst_percent' => $data['sgst_percent'],
|
||||
'igst_percent' => $data['igst_percent'],
|
||||
]);
|
||||
|
||||
$invoice->update($data);
|
||||
|
||||
Log::info("✅ Invoice Updated Successfully", [
|
||||
'invoice_id' => $invoice->id
|
||||
Log::info('✅ Invoice Updated Successfully', [
|
||||
'invoice_id' => $invoice->id,
|
||||
]);
|
||||
|
||||
$invoice->refresh();
|
||||
Log::info('🔍 Invoice AFTER UPDATE (DB values)', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'final_amount' => $invoice->final_amount,
|
||||
'gst_percent' => $invoice->gst_percent,
|
||||
'gst_amount' => $invoice->gst_amount,
|
||||
'final_amount_with_gst' => $invoice->final_amount_with_gst,
|
||||
'tax_type' => $invoice->tax_type,
|
||||
'cgst_percent' => $invoice->cgst_percent,
|
||||
'sgst_percent' => $invoice->sgst_percent,
|
||||
'igst_percent' => $invoice->igst_percent,
|
||||
]);
|
||||
|
||||
// regenerate PDF
|
||||
$this->generateInvoicePDF($invoice);
|
||||
|
||||
return redirect()
|
||||
@@ -120,33 +187,127 @@ class AdminInvoiceController extends Controller
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PDF GENERATION USING mPDF
|
||||
// UPDATE INVOICE ITEMS
|
||||
// -------------------------------------------------------------
|
||||
public function updateItems(Request $request, Invoice $invoice)
|
||||
{
|
||||
Log::info('🟡 Invoice Items Update Request', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'payload' => $request->all(),
|
||||
]);
|
||||
|
||||
$data = $request->validate([
|
||||
'items' => ['required', 'array'],
|
||||
'items.*.price' => ['required', 'numeric', 'min:0'],
|
||||
'items.*.ttl_amount' => ['required', 'numeric', 'min:0'],
|
||||
]);
|
||||
|
||||
$itemsInput = $data['items'];
|
||||
|
||||
foreach ($itemsInput as $itemId => $itemData) {
|
||||
$item = InvoiceItem::where('id', $itemId)
|
||||
->where('invoice_id', $invoice->id)
|
||||
->first();
|
||||
|
||||
if (!$item) {
|
||||
Log::warning('Invoice item not found or mismatched invoice', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'item_id' => $itemId,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$item->price = $itemData['price'];
|
||||
$item->ttl_amount = $itemData['ttl_amount'];
|
||||
$item->save();
|
||||
}
|
||||
|
||||
$newBaseAmount = InvoiceItem::where('invoice_id', $invoice->id)
|
||||
->sum('ttl_amount');
|
||||
|
||||
$taxType = $invoice->tax_type;
|
||||
$cgstPercent = (float) ($invoice->cgst_percent ?? 0);
|
||||
$sgstPercent = (float) ($invoice->sgst_percent ?? 0);
|
||||
$igstPercent = (float) ($invoice->igst_percent ?? 0);
|
||||
|
||||
$gstPercent = 0;
|
||||
if ($taxType === 'gst') {
|
||||
$gstPercent = $cgstPercent + $sgstPercent;
|
||||
} elseif ($taxType === 'igst') {
|
||||
$gstPercent = $igstPercent;
|
||||
}
|
||||
|
||||
$gstAmount = $newBaseAmount * $gstPercent / 100;
|
||||
$finalWithGst = $newBaseAmount + $gstAmount;
|
||||
|
||||
$invoice->final_amount = $newBaseAmount;
|
||||
$invoice->gst_amount = $gstAmount;
|
||||
$invoice->final_amount_with_gst = $finalWithGst;
|
||||
$invoice->gst_percent = $gstPercent;
|
||||
$invoice->save();
|
||||
|
||||
// ⭐ Total Charges (groups समावेत) पुन्हा कॅलक्युलेट
|
||||
$chargeGroupsTotal = $invoice->chargeGroups()->sum('total_charge');
|
||||
$invoice->charge_groups_total = $chargeGroupsTotal;
|
||||
$invoice->grand_total_with_charges = $invoice->final_amount_with_gst + $chargeGroupsTotal;
|
||||
$invoice->save();
|
||||
|
||||
Log::info('✅ Invoice items updated & totals recalculated', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'final_amount' => $invoice->final_amount,
|
||||
'gst_amount' => $invoice->gst_amount,
|
||||
'final_amount_with_gst' => $invoice->final_amount_with_gst,
|
||||
'tax_type' => $invoice->tax_type,
|
||||
'cgst_percent' => $invoice->cgst_percent,
|
||||
'sgst_percent' => $invoice->sgst_percent,
|
||||
'igst_percent' => $invoice->igst_percent,
|
||||
'charge_groups_total' => $invoice->charge_groups_total,
|
||||
'grand_total_with_charges' => $invoice->grand_total_with_charges,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Invoice items updated successfully.');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PDF GENERATION
|
||||
// -------------------------------------------------------------
|
||||
public function generateInvoicePDF($invoice)
|
||||
{
|
||||
$invoice->load(['items', 'order.shipments']);
|
||||
$shipment = $invoice->order?->shipments?->first();
|
||||
$invoice->load(['items', 'customer', 'container']);
|
||||
$shipment = null;
|
||||
|
||||
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
|
||||
$folder = public_path('invoices/');
|
||||
$folder = public_path('invoices/');
|
||||
|
||||
if (!file_exists($folder)) {
|
||||
mkdir($folder, 0777, true);
|
||||
}
|
||||
|
||||
$filePath = $folder . $fileName;
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
$mpdf = new Mpdf(['mode' => 'utf-8', 'format' => 'A4', 'default_font' => 'sans-serif']);
|
||||
$html = view('admin.pdf.invoice', ['invoice' => $invoice, 'shipment' => $shipment])->render();
|
||||
$mpdf = new Mpdf([
|
||||
'mode' => 'utf-8',
|
||||
'format' => 'A4',
|
||||
'default_font' => 'sans-serif',
|
||||
]);
|
||||
|
||||
$html = view('admin.pdf.invoice', [
|
||||
'invoice' => $invoice,
|
||||
'shipment' => $shipment,
|
||||
])->render();
|
||||
|
||||
$mpdf->WriteHTML($html);
|
||||
$mpdf->Output($filePath, 'F');
|
||||
|
||||
$invoice->update(['pdf_path' => 'invoices/' . $fileName]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INSTALLMENTS (ADD/DELETE)
|
||||
// INSTALLMENTS (ADD)
|
||||
// -------------------------------------------------------------
|
||||
public function storeInstallment(Request $request, $invoice_id)
|
||||
{
|
||||
@@ -157,16 +318,17 @@ class AdminInvoiceController extends Controller
|
||||
'amount' => 'required|numeric|min:1',
|
||||
]);
|
||||
|
||||
$invoice = Invoice::findOrFail($invoice_id);
|
||||
|
||||
$invoice = Invoice::findOrFail($invoice_id);
|
||||
$paidTotal = $invoice->installments()->sum('amount');
|
||||
// Use GST-inclusive total for all calculations/checks
|
||||
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
||||
|
||||
// 👇 Total Charges (grand_total_with_charges) वरून remaining
|
||||
$grandTotal = $invoice->grand_total_with_charges;
|
||||
$remaining = $grandTotal - $paidTotal;
|
||||
|
||||
if ($request->amount > $remaining) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Installment amount exceeds remaining balance.'
|
||||
'status' => 'error',
|
||||
'message' => 'Installment amount exceeds remaining balance.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
@@ -180,48 +342,114 @@ class AdminInvoiceController extends Controller
|
||||
|
||||
$newPaid = $paidTotal + $request->amount;
|
||||
|
||||
// Mark as 'paid' if GST-inclusive total is cleared
|
||||
if ($newPaid >= $invoice->final_amount_with_gst) {
|
||||
$invoice->update(['status' => 'paid']);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Installment added successfully.',
|
||||
'installment' => $installment,
|
||||
'totalPaid' => $newPaid,
|
||||
'gstAmount' => $invoice->gst_amount,
|
||||
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
||||
'baseAmount' => $invoice->final_amount,
|
||||
'remaining' => max(0, $invoice->final_amount_with_gst - $newPaid),
|
||||
'isCompleted' => $newPaid >= $invoice->final_amount_with_gst
|
||||
'status' => 'success',
|
||||
'message' => 'Installment added successfully.',
|
||||
'installment' => $installment,
|
||||
'totalPaid' => $newPaid,
|
||||
'gstAmount' => $invoice->gst_amount,
|
||||
'finalAmountWithGst' => $grandTotal, // इथे grand total पाठव
|
||||
'baseAmount' => $invoice->final_amount,
|
||||
'remaining' => max(0, $grandTotal - $newPaid),
|
||||
'isCompleted' => $newPaid >= $grandTotal,
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INSTALLMENTS (DELETE)
|
||||
// -------------------------------------------------------------
|
||||
public function deleteInstallment($id)
|
||||
{
|
||||
$installment = InvoiceInstallment::findOrFail($id);
|
||||
$invoice = $installment->invoice;
|
||||
|
||||
$invoice = $installment->invoice;
|
||||
|
||||
$installment->delete();
|
||||
|
||||
$paidTotal = $invoice->installments()->sum('amount');
|
||||
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
||||
|
||||
// Update status if not fully paid anymore
|
||||
if ($remaining > 0 && $invoice->status === "paid") {
|
||||
$invoice->update(['status' => 'pending']);
|
||||
}
|
||||
|
||||
|
||||
$paidTotal = $invoice->installments()->sum('amount');
|
||||
$grandTotal = $invoice->grand_total_with_charges;
|
||||
$remaining = $grandTotal - $paidTotal;
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Installment deleted.',
|
||||
'totalPaid' => $paidTotal,
|
||||
'gstAmount' => $invoice->gst_amount,
|
||||
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
||||
'baseAmount' => $invoice->final_amount,
|
||||
'remaining' => $remaining,
|
||||
'isZero' => $paidTotal == 0
|
||||
'status' => 'success',
|
||||
'message' => 'Installment deleted.',
|
||||
'totalPaid' => $paidTotal,
|
||||
'gstAmount' => $invoice->gst_amount,
|
||||
'finalAmountWithGst' => $grandTotal, // इथेही
|
||||
'baseAmount' => $invoice->final_amount,
|
||||
'remaining' => $remaining,
|
||||
'isZero' => $paidTotal == 0,
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// CHARGE GROUP SAVE (no AJAX branch)
|
||||
// -------------------------------------------------------------
|
||||
public function storeChargeGroup(Request $request, $invoiceId)
|
||||
{
|
||||
$invoice = Invoice::with('items')->findOrFail($invoiceId);
|
||||
|
||||
$data = $request->validate([
|
||||
'group_name' => 'required|string|max:255',
|
||||
'basis_type' => 'required|in:ttl_qty,amount,ttl_cbm,ttl_kg',
|
||||
'basis_value' => 'required|numeric',
|
||||
'rate' => 'required|numeric|min:0.0001',
|
||||
'auto_total' => 'required|numeric|min:0.01',
|
||||
'item_ids' => 'required|array',
|
||||
'item_ids.*' => 'integer|exists:invoice_items,id',
|
||||
]);
|
||||
|
||||
$exists = InvoiceChargeGroup::where('invoice_id', $invoice->id)
|
||||
->where('group_name', $data['group_name'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return back()
|
||||
->withErrors(['group_name' => 'This group name is already used for this invoice.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
$group = InvoiceChargeGroup::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
'group_name' => $data['group_name'],
|
||||
'basis_type' => $data['basis_type'],
|
||||
'basis_value' => $data['basis_value'],
|
||||
'rate' => $data['rate'],
|
||||
'total_charge' => $data['auto_total'],
|
||||
]);
|
||||
|
||||
foreach ($data['item_ids'] as $itemId) {
|
||||
InvoiceChargeGroupItem::create([
|
||||
'group_id' => $group->id,
|
||||
'invoice_item_id' => $itemId,
|
||||
]);
|
||||
}
|
||||
|
||||
// ⭐ Charge groups नुसार Total Charges सेट करा
|
||||
$chargeGroupsTotal = $invoice->chargeGroups()->sum('total_charge');
|
||||
$grandTotal = $invoice->final_amount_with_gst + $chargeGroupsTotal;
|
||||
|
||||
$invoice->update([
|
||||
'charge_groups_total' => $chargeGroupsTotal,
|
||||
'grand_total_with_charges' => $grandTotal,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->back()
|
||||
->with('success', 'Charge group saved successfully.');
|
||||
}
|
||||
|
||||
public function download(Invoice $invoice)
|
||||
{
|
||||
if (!$invoice->pdf_path || !Storage::exists($invoice->pdf_path)) {
|
||||
return back()->with('error', 'PDF not found.');
|
||||
}
|
||||
|
||||
return Storage::download($invoice->pdf_path, $invoice->invoice_number . '.pdf');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Order;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
@@ -12,45 +11,98 @@ class AdminReportController extends Controller
|
||||
/**
|
||||
* Display the reports page with joined data
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
// -------------------------------
|
||||
// FETCH REPORT DATA
|
||||
// ONLY orders that have BOTH:
|
||||
// 1. Invoice
|
||||
// 2. Shipment
|
||||
// -------------------------------
|
||||
// public function index(Request $request)
|
||||
// {
|
||||
/*********************************************************
|
||||
* OLD FLOW (Order + Shipment + Invoice)
|
||||
* फक्त reference साठी ठेवलेला, वापरत नाही.
|
||||
*********************************************************/
|
||||
|
||||
/*
|
||||
$reports = DB::table('orders')
|
||||
->join('shipment_items', 'shipment_items.order_id', '=', 'orders.id')
|
||||
->join('shipments', 'shipments.id', '=', 'shipment_items.shipment_id')
|
||||
->join('invoices', 'invoices.order_id', '=', 'orders.id')
|
||||
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'orders.mark_no')
|
||||
->leftJoin('users', 'users.customer_id', '=', 'mark_list.customer_id')
|
||||
->select(...)
|
||||
->orderBy('shipments.shipment_date', 'desc')
|
||||
->get();
|
||||
*/
|
||||
|
||||
/*********************************************************
|
||||
* NEW FLOW (Container + Invoice + MarkList)
|
||||
*********************************************************/
|
||||
|
||||
// $reports = DB::table('invoices')
|
||||
// ->join('containers', 'containers.id', '=', 'invoices.containerid')
|
||||
// ->leftJoin('mark_list', 'mark_list.markno', '=', 'invoices.markno')
|
||||
// ->select(
|
||||
// 'invoices.id as invoicepk',
|
||||
// 'invoices.invoicenumber',
|
||||
// 'invoices.invoicedate',
|
||||
// 'invoices.finalamount',
|
||||
// 'invoices.finalamountwithgst',
|
||||
// 'invoices.gstpercent',
|
||||
// 'invoices.gstamount',
|
||||
// 'invoices.status as invoicestatus',
|
||||
// 'invoices.markno',
|
||||
|
||||
// 'containers.id as containerpk',
|
||||
// 'containers.containernumber',
|
||||
// 'containers.containerdate',
|
||||
// 'containers.containername',
|
||||
|
||||
// 'mark_list.companyname',
|
||||
// 'mark_list.customername'
|
||||
// )
|
||||
// ->orderBy('containers.containerdate', 'desc')
|
||||
// ->get();
|
||||
|
||||
// return view('admin.reports', compact('reports'));
|
||||
// }
|
||||
|
||||
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$reports = DB::table('invoices')
|
||||
->join('containers', 'containers.id', '=', 'invoices.container_id')
|
||||
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
|
||||
->select(
|
||||
'orders.id as order_pk',
|
||||
'orders.order_id',
|
||||
'orders.mark_no',
|
||||
'orders.origin',
|
||||
'orders.destination',
|
||||
|
||||
'shipments.id as shipment_pk',
|
||||
'shipments.shipment_id',
|
||||
'shipments.status as shipment_status',
|
||||
'shipments.shipment_date',
|
||||
|
||||
// INVOICE
|
||||
'invoices.id as invoicepk',
|
||||
'invoices.invoice_number',
|
||||
'invoices.invoice_date',
|
||||
'invoices.final_amount',
|
||||
'invoices.status as invoice_status',
|
||||
|
||||
'mark_list.company_name',
|
||||
'mark_list.customer_name'
|
||||
'invoices.final_amount_with_gst',
|
||||
'invoices.gst_percent',
|
||||
'invoices.gst_amount',
|
||||
'invoices.status as invoicestatus',
|
||||
'invoices.mark_no',
|
||||
|
||||
// CONTAINER
|
||||
'containers.id as containerpk',
|
||||
'containers.container_number',
|
||||
'containers.container_date',
|
||||
'containers.container_name',
|
||||
|
||||
// RAW FIELDS (for reference/debug if needed)
|
||||
'invoices.company_name as inv_company_name',
|
||||
'invoices.customer_name as inv_customer_name',
|
||||
'mark_list.company_name as ml_company_name',
|
||||
'mark_list.customer_name as ml_customer_name',
|
||||
|
||||
// FINAL FIELDS (automatically pick invoice first, else mark_list)
|
||||
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
|
||||
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name')
|
||||
)
|
||||
->orderBy('invoices.invoice_date', 'desc')
|
||||
->orderBy('invoices.id', 'desc')
|
||||
|
||||
->orderBy('shipments.shipment_date', 'desc')
|
||||
->get();
|
||||
|
||||
|
||||
return view('admin.reports', compact('reports'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ class AdminStaffController extends Controller
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
// 1️⃣ Create staff WITHOUT employee_id (ID not available yet)
|
||||
$admin = Admin::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
@@ -69,23 +70,33 @@ class AdminStaffController extends Controller
|
||||
'status' => $request->status,
|
||||
'additional_info' => $request->additional_info,
|
||||
|
||||
'username' => $request->username,
|
||||
// username may be NULL here
|
||||
'username' => $request->username ?: null,
|
||||
'password' => Hash::make($request->password),
|
||||
'type' => 'staff',
|
||||
]);
|
||||
|
||||
// Generate EMPLOYEE ID using admin ID (safe)
|
||||
// 2️⃣ Generate EMPLOYEE ID
|
||||
$employeeId = 'EMP' . str_pad($admin->id, 4, '0', STR_PAD_LEFT);
|
||||
$admin->update(['employee_id' => $employeeId]);
|
||||
|
||||
// Assign permissions (if any)
|
||||
// 3️⃣ Auto-generate username if left blank
|
||||
$username = $request->username ?: strtolower($employeeId);
|
||||
|
||||
// 4️⃣ Update employee_id + username together
|
||||
$admin->update([
|
||||
'employee_id' => $employeeId,
|
||||
'username' => $username,
|
||||
]);
|
||||
|
||||
// 5️⃣ Assign permissions (if any)
|
||||
if ($request->permissions) {
|
||||
$admin->givePermissionTo($request->permissions);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
return redirect()->route('admin.staff.index')
|
||||
return redirect()
|
||||
->route('admin.staff.index')
|
||||
->with('success', 'Staff created successfully.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
@@ -94,6 +105,7 @@ class AdminStaffController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$staff = Admin::where('type', 'staff')->findOrFail($id);
|
||||
|
||||
@@ -20,7 +20,11 @@ class ShipmentController extends Controller
|
||||
$usedOrderIds = ShipmentItem::pluck('order_id')->toArray();
|
||||
|
||||
// 2) Load available orders (not used in any shipment)
|
||||
$availableOrders = Order::whereNotIn('id', $usedOrderIds)->get();
|
||||
$availableOrders = Order::whereNotIn('id', $usedOrderIds)
|
||||
->where('status', '!=', 'order_placed')
|
||||
->get();
|
||||
|
||||
|
||||
|
||||
// 3) Load all shipments for listing
|
||||
$shipments = Shipment::latest()->get();
|
||||
@@ -65,6 +69,16 @@ class ShipmentController extends Controller
|
||||
// CALCULATE TOTALS
|
||||
// -----------------------------
|
||||
$orders = Order::whereIn('id', $request->order_ids)->get();
|
||||
foreach ($orders as $order) {
|
||||
if ($order->status === 'order_placed') {
|
||||
return back()->with(
|
||||
'error',
|
||||
"Order {$order->order_id} is not ready for shipment"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
$total_ctn = $orders->sum('ctn');
|
||||
$total_qty = $orders->sum('qty');
|
||||
@@ -82,7 +96,7 @@ class ShipmentController extends Controller
|
||||
'shipment_id' => $newShipmentId,
|
||||
'origin' => $request->origin,
|
||||
'destination' => $request->destination,
|
||||
'status' => Shipment::STATUS_PENDING,
|
||||
'status' => Shipment::STATUS_SHIPMENT_READY,
|
||||
'shipment_date' => $request->shipment_date,
|
||||
|
||||
'total_ctn' => $total_ctn,
|
||||
@@ -135,29 +149,35 @@ class ShipmentController extends Controller
|
||||
* Update Shipment status from action button
|
||||
*/
|
||||
public function updateStatus(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'shipment_id' => 'required|exists:shipments,id',
|
||||
'status' => 'required|string'
|
||||
]);
|
||||
{
|
||||
$request->validate([
|
||||
'shipment_id' => 'required|exists:shipments,id',
|
||||
'status' => 'required|string'
|
||||
]);
|
||||
|
||||
// 1) Update shipment status
|
||||
$shipment = Shipment::findOrFail($request->shipment_id);
|
||||
$shipment->status = $request->status;
|
||||
$shipment->save();
|
||||
$shipment = Shipment::findOrFail($request->shipment_id);
|
||||
$shipment->status = $request->status;
|
||||
$shipment->save();
|
||||
|
||||
// 2) Update ALL related orders' status
|
||||
foreach ($shipment->orders as $order) {
|
||||
$order->status = $shipment->status; // status is string: pending, in_transit, dispatched, delivered
|
||||
$order->save();
|
||||
// ✅ Sync shipment status to orders ONLY after shipment exists
|
||||
foreach ($shipment->orders as $order) {
|
||||
|
||||
// Prevent rollback or overwrite
|
||||
if ($order->status === 'delivered') {
|
||||
continue;
|
||||
}
|
||||
|
||||
return redirect()->back()->with(
|
||||
'success',
|
||||
"Shipment status updated to {$shipment->statusLabel()} and related orders updated."
|
||||
);
|
||||
$order->status = $shipment->status;
|
||||
$order->save();
|
||||
}
|
||||
|
||||
return redirect()->back()->with(
|
||||
'success',
|
||||
"Shipment status updated to {$shipment->statusLabel()}."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update shipment details
|
||||
*/
|
||||
@@ -224,5 +244,95 @@ class ShipmentController extends Controller
|
||||
|
||||
return view('admin.view_shipment', compact('shipment', 'dummyData'));
|
||||
}
|
||||
// App\Models\Shipment.php
|
||||
|
||||
public function orders()
|
||||
{
|
||||
return $this->belongsToMany(\App\Models\Order::class, 'shipment_items', 'shipment_id', 'order_id');
|
||||
}
|
||||
|
||||
public function removeOrder(Shipment $shipment, Order $order)
|
||||
{
|
||||
// Remove row from pivot table shipment_items
|
||||
ShipmentItem::where('shipment_id', $shipment->id)
|
||||
->where('order_id', $order->id)
|
||||
->delete(); // removes link shipment <-> order [web:41][web:45]
|
||||
|
||||
// Recalculate totals on this shipment (optional but recommended)
|
||||
$orders = Order::whereIn(
|
||||
'id',
|
||||
ShipmentItem::where('shipment_id', $shipment->id)->pluck('order_id')
|
||||
)->get();
|
||||
|
||||
$shipment->total_ctn = $orders->sum('ctn');
|
||||
$shipment->total_qty = $orders->sum('qty');
|
||||
$shipment->total_ttl_qty = $orders->sum('ttl_qty');
|
||||
$shipment->total_cbm = $orders->sum('cbm');
|
||||
$shipment->total_ttl_cbm = $orders->sum('ttl_cbm');
|
||||
$shipment->total_kg = $orders->sum('kg');
|
||||
$shipment->total_ttl_kg = $orders->sum('ttl_kg');
|
||||
$shipment->total_amount = $orders->sum('ttl_amount');
|
||||
$shipment->save();
|
||||
|
||||
// Redirect back to preview page where your blade is loaded
|
||||
return redirect()
|
||||
->route('admin.shipments.dummy', $shipment->id)
|
||||
->with('success', 'Order removed from shipment successfully.');
|
||||
}
|
||||
|
||||
public function addOrders(Request $request, Shipment $shipment)
|
||||
{
|
||||
$request->validate([
|
||||
'order_ids' => 'required|array|min:1',
|
||||
]);
|
||||
|
||||
$orders = Order::whereIn('id', $request->order_ids)->get();
|
||||
|
||||
foreach ($orders as $order) {
|
||||
|
||||
if ($order->status === 'order_placed') {
|
||||
return back()->with(
|
||||
'error',
|
||||
"Order {$order->order_id} is not ready for shipment"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Prevent duplicates
|
||||
if (ShipmentItem::where('order_id', $order->id)->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ShipmentItem::create([
|
||||
'shipment_id' => $shipment->id,
|
||||
'order_id' => $order->id,
|
||||
'order_ctn' => $order->ctn,
|
||||
'order_qty' => $order->qty,
|
||||
'order_ttl_qty' => $order->ttl_qty,
|
||||
'order_ttl_amount' => $order->ttl_amount,
|
||||
'order_ttl_kg' => $order->ttl_kg,
|
||||
]);
|
||||
}
|
||||
|
||||
// Recalculate totals
|
||||
$orderIds = ShipmentItem::where('shipment_id', $shipment->id)->pluck('order_id');
|
||||
$allOrders = Order::whereIn('id', $orderIds)->get();
|
||||
|
||||
$shipment->update([
|
||||
'total_ctn' => $allOrders->sum('ctn'),
|
||||
'total_qty' => $allOrders->sum('qty'),
|
||||
'total_ttl_qty' => $allOrders->sum('ttl_qty'),
|
||||
'total_cbm' => $allOrders->sum('cbm'),
|
||||
'total_ttl_cbm' => $allOrders->sum('ttl_cbm'),
|
||||
'total_kg' => $allOrders->sum('kg'),
|
||||
'total_ttl_kg' => $allOrders->sum('ttl_kg'),
|
||||
'total_amount' => $allOrders->sum('ttl_amount'),
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.shipments.dummy', $shipment->id)
|
||||
->with('success', 'Orders added to shipment successfully.');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -15,7 +15,12 @@ class UserRequestController extends Controller
|
||||
public function index()
|
||||
{
|
||||
$requests = CustomerRequest::orderBy('id', 'desc')->get();
|
||||
return view('admin.requests', compact('requests'));
|
||||
$pendingProfileUpdates = \App\Models\UpdateRequest::where('status', 'pending')->count();
|
||||
|
||||
return view('admin.requests', compact(
|
||||
'requests',
|
||||
'pendingProfileUpdates'
|
||||
));
|
||||
}
|
||||
|
||||
// Approve user request
|
||||
@@ -80,7 +85,6 @@ public function approveProfileUpdate($id)
|
||||
$req = \App\Models\UpdateRequest::findOrFail($id);
|
||||
$user = \App\Models\User::findOrFail($req->user_id);
|
||||
|
||||
// FIX: Ensure data is array
|
||||
$newData = is_array($req->data) ? $req->data : json_decode($req->data, true);
|
||||
|
||||
foreach ($newData as $key => $value) {
|
||||
@@ -91,8 +95,18 @@ public function approveProfileUpdate($id)
|
||||
}
|
||||
}
|
||||
|
||||
// Update user table
|
||||
$user->save();
|
||||
|
||||
// Update mark_list table
|
||||
\App\Models\MarkList::where('customer_id', $user->customer_id)
|
||||
->update([
|
||||
'customer_name' => $user->customer_name,
|
||||
'company_name' => $user->company_name,
|
||||
'mobile_no' => $user->mobile_no
|
||||
]);
|
||||
|
||||
// Update request status
|
||||
$req->status = 'approved';
|
||||
$req->admin_note = 'Approved by admin on ' . now();
|
||||
$req->save();
|
||||
|
||||
943
app/Http/Controllers/ContainerController.php
Normal file
943
app/Http/Controllers/ContainerController.php
Normal file
@@ -0,0 +1,943 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Container;
|
||||
use App\Models\ContainerRow;
|
||||
use App\Models\MarkList;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
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
|
||||
|
||||
class ContainerController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$containers = Container::with('rows')->latest()->get();
|
||||
|
||||
$containers->each(function ($container) {
|
||||
$rows = $container->rows;
|
||||
|
||||
$totalCtn = 0;
|
||||
$totalQty = 0;
|
||||
$totalCbm = 0;
|
||||
$totalKg = 0;
|
||||
|
||||
$ctnKeys = ['CTN', 'CTNS'];
|
||||
$qtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY', 'QTY', 'PCS', 'PIECES'];
|
||||
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
|
||||
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
|
||||
|
||||
$getFirstNumeric = function (array $data, array $possibleKeys) {
|
||||
$normalizedMap = [];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key === null || $key === '') continue;
|
||||
|
||||
$normKey = strtoupper((string)$key);
|
||||
$normKey = str_replace([' ', '/', '-', '.'], '', $normKey);
|
||||
$normalizedMap[$normKey] = $value;
|
||||
}
|
||||
|
||||
foreach ($possibleKeys as $search) {
|
||||
$normSearch = strtoupper($search);
|
||||
$normSearch = str_replace([' ', '/', '-', '.'], '', $normSearch);
|
||||
|
||||
foreach ($normalizedMap as $nKey => $value) {
|
||||
if (
|
||||
strpos($nKey, $normSearch) !== false &&
|
||||
(is_numeric($value) || (is_string($value) && is_numeric(trim($value))))
|
||||
) {
|
||||
return (float) trim($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$data = $row->data ?? [];
|
||||
|
||||
$totalCtn += $getFirstNumeric($data, $ctnKeys);
|
||||
$totalQty += $getFirstNumeric($data, $qtyKeys);
|
||||
$totalCbm += $getFirstNumeric($data, $cbmKeys);
|
||||
$totalKg += $getFirstNumeric($data, $kgKeys);
|
||||
}
|
||||
|
||||
$container->summary = [
|
||||
'total_ctn' => round($totalCtn, 2),
|
||||
'total_qty' => round($totalQty, 2),
|
||||
'total_cbm' => round($totalCbm, 3),
|
||||
'total_kg' => round($totalKg, 2),
|
||||
];
|
||||
});
|
||||
|
||||
return view('admin.container', compact('containers'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('admin.container_create');
|
||||
}
|
||||
|
||||
private function isValidExcelFormat($rows, $header)
|
||||
{
|
||||
if (empty($header) || count($rows) < 2) return false;
|
||||
|
||||
$validKeywords = [
|
||||
'MARK', 'DESCRIPTION', 'DESC', 'CTN', 'CTNS', 'QTY', 'TOTALQTY', 'ITLQTY', 'ITL QTY',
|
||||
'UNIT', 'CBM', 'TOTAL CBM', 'KG', 'TOTAL KG',
|
||||
'METAL BUCKLE', 'WATCH MOVEMENT', 'STEEL BOTTLE',
|
||||
'MEHULPAID', 'ITEM NO', 'ITEM NO.', 'SAHILPAID', 'PINAKIN', 'GST',
|
||||
'MOON LAMP', 'TRANSPARENT BOTTLE', 'PLASTIC FONDANT',
|
||||
];
|
||||
|
||||
$headerText = implode(' ', array_map('strtoupper', $header));
|
||||
$requiredHeaders = ['CTN', 'QTY', 'DESCRIPTION', 'DESC'];
|
||||
|
||||
$hasValidHeaders = false;
|
||||
foreach ($requiredHeaders as $key) {
|
||||
if (stripos($headerText, $key) !== false) {
|
||||
$hasValidHeaders = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$hasValidHeaders) return false;
|
||||
|
||||
$dataPreview = '';
|
||||
for ($i = 0; $i < min(5, count($rows)); $i++) {
|
||||
$rowText = implode(' ', array_slice($rows[$i], 0, 10));
|
||||
$dataPreview .= ' ' . strtoupper((string)$rowText);
|
||||
}
|
||||
|
||||
$validMatches = 0;
|
||||
foreach ($validKeywords as $keyword) {
|
||||
if (stripos($headerText . $dataPreview, strtoupper($keyword)) !== false) {
|
||||
$validMatches++;
|
||||
}
|
||||
}
|
||||
|
||||
return $validMatches >= 3;
|
||||
}
|
||||
|
||||
private function normalizeKey($value): string
|
||||
{
|
||||
$norm = strtoupper((string)$value);
|
||||
return str_replace([' ', '/', '-', '.'], '', $norm);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'container_name' => 'required|string',
|
||||
'container_number' => 'required|string|unique:containers,container_number',
|
||||
'container_date' => 'required|date',
|
||||
'excel_file' => 'required|file|mimes:xls,xlsx',
|
||||
]);
|
||||
|
||||
$file = $request->file('excel_file');
|
||||
$sheets = Excel::toArray([], $file);
|
||||
$rows = $sheets[0] ?? [];
|
||||
|
||||
if (count($rows) < 2) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'Excel file is empty.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
// HEADER DETECTION
|
||||
$headerRowIndex = null;
|
||||
$header = [];
|
||||
|
||||
foreach ($rows as $i => $row) {
|
||||
$trimmed = array_map(fn($v) => trim((string)$v), $row);
|
||||
$nonEmpty = array_filter($trimmed, fn($v) => $v !== '');
|
||||
if (empty($nonEmpty)) continue;
|
||||
|
||||
if (count($nonEmpty) >= 4) {
|
||||
$headerRowIndex = $i;
|
||||
$header = $trimmed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($headerRowIndex === null) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'Header row not found in Excel.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
if (!$this->isValidExcelFormat($rows, $header)) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'Only MEHUL / SAHIL / PINAKIN / GST loading list formats allowed.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
// COLUMN INDEXES
|
||||
$essentialColumns = [
|
||||
'desc_col' => null,
|
||||
'ctn_col' => null,
|
||||
'qty_col' => null,
|
||||
'totalqty_col' => null,
|
||||
'unit_col' => null,
|
||||
'price_col' => null,
|
||||
'amount_col' => null,
|
||||
'cbm_col' => null,
|
||||
'totalcbm_col' => null,
|
||||
'kg_col' => null,
|
||||
'totalkg_col' => null,
|
||||
'itemno_col' => null,
|
||||
];
|
||||
|
||||
foreach ($header as $colIndex => $headingText) {
|
||||
if (empty($headingText)) continue;
|
||||
|
||||
$normalized = $this->normalizeKey($headingText);
|
||||
|
||||
if (strpos($normalized, 'DESCRIPTION') !== false || strpos($normalized, 'DESC') !== false) {
|
||||
$essentialColumns['desc_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'CTN') !== false || strpos($normalized, 'CTNS') !== false) {
|
||||
$essentialColumns['ctn_col'] = $colIndex;
|
||||
} elseif (
|
||||
strpos($normalized, 'ITLQTY') !== false ||
|
||||
strpos($normalized, 'TOTALQTY') !== false ||
|
||||
strpos($normalized, 'TTLQTY') !== false
|
||||
) {
|
||||
$essentialColumns['totalqty_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'QTY') !== false) {
|
||||
$essentialColumns['qty_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'UNIT') !== false) {
|
||||
$essentialColumns['unit_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'PRICE') !== false) {
|
||||
$essentialColumns['price_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'AMOUNT') !== false) {
|
||||
$essentialColumns['amount_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'TOTALCBM') !== false || strpos($normalized, 'ITLCBM') !== false) {
|
||||
$essentialColumns['totalcbm_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'CBM') !== false) {
|
||||
$essentialColumns['cbm_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'TOTALKG') !== false || strpos($normalized, 'TTKG') !== false) {
|
||||
$essentialColumns['totalkg_col'] = $colIndex;
|
||||
} elseif (strpos($normalized, 'KG') !== false) {
|
||||
$essentialColumns['kg_col'] = $colIndex;
|
||||
} elseif (
|
||||
strpos($normalized, 'MARKNO') !== false ||
|
||||
strpos($normalized, 'MARK') !== false ||
|
||||
strpos($normalized, 'ITEMNO') !== false ||
|
||||
strpos($normalized, 'ITEM') !== false
|
||||
) {
|
||||
$essentialColumns['itemno_col'] = $colIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($essentialColumns['itemno_col'])) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'Mark / Item column not found in Excel (expected headers like MARK NO / Mark_No / Item_No).'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
// ROWS CLEANING
|
||||
$dataRows = array_slice($rows, $headerRowIndex + 1);
|
||||
$cleanedRows = [];
|
||||
$unmatchedRowsData = [];
|
||||
|
||||
foreach ($dataRows as $offset => $row) {
|
||||
$trimmedRow = array_map(fn($v) => trim((string)$v), $row);
|
||||
$nonEmptyCells = array_filter($trimmedRow, fn($v) => $v !== '');
|
||||
if (count($nonEmptyCells) < 2) continue;
|
||||
|
||||
$rowText = strtoupper(implode(' ', $trimmedRow));
|
||||
if (
|
||||
stripos($rowText, 'TOTAL') !== false ||
|
||||
stripos($rowText, 'TTL') !== false ||
|
||||
stripos($rowText, 'GRAND') !== false
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$descValue = '';
|
||||
if ($essentialColumns['desc_col'] !== null) {
|
||||
$descValue = trim($row[$essentialColumns['desc_col']] ?? '');
|
||||
}
|
||||
|
||||
if ($essentialColumns['desc_col'] !== null && $descValue === '' && count($nonEmptyCells) >= 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cleanedRows[] = [
|
||||
'row' => $row,
|
||||
'offset' => $offset,
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($cleanedRows)) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'No valid item rows found in Excel.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
// FORMULA CHECK
|
||||
$cleanNumber = function ($value) {
|
||||
if (is_string($value)) {
|
||||
$value = str_replace(',', '', trim($value));
|
||||
}
|
||||
return is_numeric($value) ? (float)$value : 0;
|
||||
};
|
||||
|
||||
$formulaErrors = [];
|
||||
|
||||
foreach ($cleanedRows as $item) {
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
|
||||
$ctn = $essentialColumns['ctn_col'] !== null ? $cleanNumber($row[$essentialColumns['ctn_col']] ?? 0) : 0;
|
||||
$qty = $essentialColumns['qty_col'] !== null ? $cleanNumber($row[$essentialColumns['qty_col']] ?? 0) : 0;
|
||||
$ttlQ = $essentialColumns['totalqty_col'] !== null ? $cleanNumber($row[$essentialColumns['totalqty_col']] ?? 0) : 0;
|
||||
$cbm = $essentialColumns['cbm_col'] !== null ? $cleanNumber($row[$essentialColumns['cbm_col']] ?? 0) : 0;
|
||||
$ttlC = $essentialColumns['totalcbm_col'] !== null ? $cleanNumber($row[$essentialColumns['totalcbm_col']] ?? 0) : 0;
|
||||
$kg = $essentialColumns['kg_col'] !== null ? $cleanNumber($row[$essentialColumns['kg_col']] ?? 0) : 0;
|
||||
$ttlK = $essentialColumns['totalkg_col'] !== null ? $cleanNumber($row[$essentialColumns['totalkg_col']] ?? 0) : 0;
|
||||
|
||||
$price = $essentialColumns['price_col'] !== null ? $cleanNumber($row[$essentialColumns['price_col']] ?? 0) : 0;
|
||||
$ttlAmount = $essentialColumns['amount_col'] !== null ? $cleanNumber($row[$essentialColumns['amount_col']] ?? 0) : 0;
|
||||
|
||||
$desc = $essentialColumns['desc_col'] !== null ? (string)($row[$essentialColumns['desc_col']] ?? '') : '';
|
||||
$mark = $essentialColumns['itemno_col'] !== null ? (string)($row[$essentialColumns['itemno_col']] ?? '') : '';
|
||||
|
||||
$expTtlQty = $qty * $ctn;
|
||||
$expTtlCbm = $cbm * $ctn;
|
||||
$expTtlKg = $kg * $ctn;
|
||||
$expTtlAmount = ($qty * $ctn) * $price;
|
||||
|
||||
$rowErrors = [];
|
||||
|
||||
if (abs($ttlQ - $expTtlQty) > 0.01) {
|
||||
$rowErrors['TOTAL QTY'] = [
|
||||
'actual' => $ttlQ,
|
||||
'expected' => $expTtlQty,
|
||||
];
|
||||
}
|
||||
|
||||
if (abs($ttlC - $expTtlCbm) > 0.0005) {
|
||||
$rowErrors['TOTAL CBM'] = [
|
||||
'actual' => $ttlC,
|
||||
'expected' => $expTtlCbm,
|
||||
];
|
||||
}
|
||||
|
||||
if (abs($ttlK - $expTtlKg) > 0.01) {
|
||||
$rowErrors['TOTAL KG'] = [
|
||||
'actual' => $ttlK,
|
||||
'expected' => $expTtlKg,
|
||||
];
|
||||
}
|
||||
|
||||
if ($essentialColumns['amount_col'] !== null && $essentialColumns['price_col'] !== null) {
|
||||
if (abs($ttlAmount - $expTtlAmount) > 0.01) {
|
||||
$rowErrors['TOTAL AMOUNT'] = [
|
||||
'actual' => $ttlAmount,
|
||||
'expected' => $expTtlAmount,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($rowErrors)) {
|
||||
$rowData = [];
|
||||
foreach ($header as $colIndex => $headingText) {
|
||||
$value = $row[$colIndex] ?? null;
|
||||
if (is_string($value)) $value = trim($value);
|
||||
$rowData[$headingText] = $value;
|
||||
}
|
||||
|
||||
$formulaErrors[] = [
|
||||
'excel_row' => $headerRowIndex + 1 + $offset,
|
||||
'mark_no' => $mark,
|
||||
'description' => $desc,
|
||||
'errors' => $rowErrors,
|
||||
'data' => $rowData,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// MARK CHECK
|
||||
$marksFromExcel = [];
|
||||
foreach ($cleanedRows as $item) {
|
||||
$row = $item['row'];
|
||||
$rawMark = $row[$essentialColumns['itemno_col']] ?? null;
|
||||
$mark = trim((string)($rawMark ?? ''));
|
||||
if ($mark !== '') {
|
||||
$marksFromExcel[] = $mark;
|
||||
}
|
||||
}
|
||||
|
||||
$marksFromExcel = array_values(array_unique($marksFromExcel));
|
||||
|
||||
if (empty($marksFromExcel)) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'No mark numbers found in Excel file.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
$validMarks = MarkList::whereIn('mark_no', $marksFromExcel)
|
||||
->where('status', 'active')
|
||||
->pluck('mark_no')
|
||||
->toArray();
|
||||
|
||||
$unmatchedMarks = array_values(array_diff($marksFromExcel, $validMarks));
|
||||
|
||||
$markErrors = [];
|
||||
|
||||
if (!empty($unmatchedMarks)) {
|
||||
foreach ($cleanedRows as $item) {
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
$rowMark = trim((string)($row[$essentialColumns['itemno_col']] ?? ''));
|
||||
|
||||
if ($rowMark === '' || !in_array($rowMark, $unmatchedMarks)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rowData = [];
|
||||
foreach ($header as $colIndex => $headingText) {
|
||||
$value = $row[$colIndex] ?? null;
|
||||
if (is_string($value)) $value = trim($value);
|
||||
$rowData[$headingText] = $value;
|
||||
}
|
||||
|
||||
$markErrors[] = [
|
||||
'excel_row' => $headerRowIndex + 1 + $offset,
|
||||
'mark_no' => $rowMark,
|
||||
'data' => $rowData,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($formulaErrors) || !empty($markErrors)) {
|
||||
return back()
|
||||
->withInput()
|
||||
->with([
|
||||
'formula_errors' => $formulaErrors,
|
||||
'mark_errors' => $markErrors,
|
||||
]);
|
||||
}
|
||||
|
||||
// STEP 1: Marks → customers mapping + grouping
|
||||
$markRecords = MarkList::whereIn('mark_no', $marksFromExcel)
|
||||
->where('status', 'active')
|
||||
->get();
|
||||
|
||||
$markToCustomerId = [];
|
||||
$markToSnapshot = [];
|
||||
|
||||
foreach ($markRecords as $mr) {
|
||||
$markToCustomerId[$mr->mark_no] = $mr->customer_id;
|
||||
|
||||
$markToSnapshot[$mr->mark_no] = [
|
||||
'customer_name' => $mr->customer_name,
|
||||
'company_name' => $mr->company_name,
|
||||
'mobile_no' => $mr->mobile_no,
|
||||
];
|
||||
}
|
||||
|
||||
$groupedByCustomer = [];
|
||||
|
||||
foreach ($cleanedRows as $item) {
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
|
||||
$rawMark = $row[$essentialColumns['itemno_col']] ?? null;
|
||||
$mark = trim((string)($rawMark ?? ''));
|
||||
|
||||
if ($mark === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$customerId = $markToCustomerId[$mark] ?? null;
|
||||
if (!$customerId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($groupedByCustomer[$customerId])) {
|
||||
$groupedByCustomer[$customerId] = [];
|
||||
}
|
||||
|
||||
$groupedByCustomer[$customerId][] = [
|
||||
'row' => $row,
|
||||
'offset' => $offset,
|
||||
'mark' => $mark,
|
||||
];
|
||||
}
|
||||
|
||||
// STEP 2: Container + ContainerRows save
|
||||
$container = Container::create([
|
||||
'container_name' => $request->container_name,
|
||||
'container_number' => $request->container_number,
|
||||
'container_date' => $request->container_date,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
$path = $file->store('containers');
|
||||
$container->update(['excel_file' => $path]);
|
||||
|
||||
$savedCount = 0;
|
||||
|
||||
foreach ($cleanedRows as $item) {
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
|
||||
$data = [];
|
||||
foreach ($header as $colIndex => $headingText) {
|
||||
$value = $row[$colIndex] ?? null;
|
||||
if (is_string($value)) $value = trim($value);
|
||||
$data[$headingText] = $value;
|
||||
}
|
||||
|
||||
ContainerRow::create([
|
||||
'container_id' => $container->id,
|
||||
'row_index' => $headerRowIndex + 1 + $offset,
|
||||
'data' => $data,
|
||||
]);
|
||||
|
||||
$savedCount++;
|
||||
}
|
||||
|
||||
// STEP 3: per-customer invoices + invoice items
|
||||
$invoiceCount = 0;
|
||||
|
||||
foreach ($groupedByCustomer as $customerId => $rowsForCustomer) {
|
||||
if (empty($rowsForCustomer)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$firstMark = $rowsForCustomer[0]['mark'];
|
||||
$snap = $markToSnapshot[$firstMark] ?? null;
|
||||
|
||||
$invoice = new Invoice();
|
||||
$invoice->container_id = $container->id;
|
||||
// $invoice->customer_id = $customerId;
|
||||
$invoice->mark_no = $firstMark;
|
||||
|
||||
$invoice->invoice_number = $this->generateInvoiceNumber();
|
||||
|
||||
// invoice_date = container_date
|
||||
$invoice->invoice_date = $container->container_date;
|
||||
|
||||
// due_date = container_date + 10 days
|
||||
$invoice->due_date = Carbon::parse($invoice->invoice_date)
|
||||
->addDays(10)
|
||||
->format('Y-m-d');
|
||||
|
||||
if ($snap) {
|
||||
$invoice->customer_name = $snap['customer_name'] ?? null;
|
||||
$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;
|
||||
|
||||
$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);
|
||||
$invoice->pdf_path = null;
|
||||
$invoice->status = 'pending';
|
||||
|
||||
$invoice->save();
|
||||
$invoiceCount++;
|
||||
|
||||
$totalAmount = 0;
|
||||
|
||||
foreach ($rowsForCustomer as $item) {
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
|
||||
$description = $essentialColumns['desc_col'] !== null ? ($row[$essentialColumns['desc_col']] ?? null) : null;
|
||||
$ctn = $essentialColumns['ctn_col'] !== null ? (int) ($row[$essentialColumns['ctn_col']] ?? 0) : 0;
|
||||
$qty = $essentialColumns['qty_col'] !== null ? (int) ($row[$essentialColumns['qty_col']] ?? 0) : 0;
|
||||
$ttlQty = $essentialColumns['totalqty_col'] !== null ? (int) ($row[$essentialColumns['totalqty_col']] ?? 0) : $qty;
|
||||
$unit = $essentialColumns['unit_col'] !== null ? ($row[$essentialColumns['unit_col']] ?? null) : null;
|
||||
$price = $essentialColumns['price_col'] !== null ? (float) ($row[$essentialColumns['price_col']] ?? 0) : 0;
|
||||
$ttlAmount = $essentialColumns['amount_col'] !== null ? (float) ($row[$essentialColumns['amount_col']] ?? 0) : 0;
|
||||
$cbm = $essentialColumns['cbm_col'] !== null ? (float) ($row[$essentialColumns['cbm_col']] ?? 0) : 0;
|
||||
$ttlCbm = $essentialColumns['totalcbm_col'] !== null ? (float) ($row[$essentialColumns['totalcbm_col']] ?? $cbm) : $cbm;
|
||||
$kg = $essentialColumns['kg_col'] !== null ? (float) ($row[$essentialColumns['kg_col']] ?? 0) : 0;
|
||||
$ttlKg = $essentialColumns['totalkg_col'] !== null ? (float) ($row[$essentialColumns['totalkg_col']] ?? $kg) : $kg;
|
||||
|
||||
$rowIndex = $headerRowIndex + 1 + $offset;
|
||||
|
||||
InvoiceItem::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
'container_id' => $container->id,
|
||||
'container_row_index' => $rowIndex,
|
||||
'description' => $description,
|
||||
'ctn' => $ctn,
|
||||
'qty' => $qty,
|
||||
'ttl_qty' => $ttlQty,
|
||||
'unit' => $unit,
|
||||
'price' => $price,
|
||||
'ttl_amount' => $ttlAmount,
|
||||
'cbm' => $cbm,
|
||||
'ttl_cbm' => $ttlCbm,
|
||||
'kg' => $kg,
|
||||
'ttl_kg' => $ttlKg,
|
||||
'shop_no' => null,
|
||||
]);
|
||||
|
||||
$totalAmount += $ttlAmount;
|
||||
}
|
||||
|
||||
$invoice->final_amount = $totalAmount;
|
||||
$invoice->gst_percent = 0;
|
||||
$invoice->gst_amount = 0;
|
||||
$invoice->final_amount_with_gst = $totalAmount;
|
||||
|
||||
$invoice->save();
|
||||
}
|
||||
|
||||
$msg = "Container '{$container->container_number}' created with {$savedCount} rows and {$invoiceCount} customer invoice(s).";
|
||||
return redirect()->route('containers.index')->with('success', $msg);
|
||||
}
|
||||
|
||||
public function show(Container $container)
|
||||
{
|
||||
$container->load('rows');
|
||||
return view('admin.container_show', compact('container'));
|
||||
}
|
||||
|
||||
public function updateRows(Request $request, Container $container)
|
||||
{
|
||||
$rowsInput = $request->input('rows', []);
|
||||
|
||||
foreach ($rowsInput as $rowId => $cols) {
|
||||
$row = ContainerRow::where('container_id', $container->id)
|
||||
->where('id', $rowId)
|
||||
->first();
|
||||
|
||||
if (!$row) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1) update container_rows.data
|
||||
$data = $row->data ?? [];
|
||||
foreach ($cols as $colHeader => $value) {
|
||||
$data[$colHeader] = $value;
|
||||
}
|
||||
$row->update(['data' => $data]);
|
||||
|
||||
// 2) normalize keys
|
||||
$normalizedMap = [];
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key === null || $key === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normKey = strtoupper((string)$key);
|
||||
$normKey = str_replace([' ', '/', '-', '.'], '', $normKey);
|
||||
$normalizedMap[$normKey] = $value;
|
||||
}
|
||||
|
||||
// helper: get first numeric value from given keys
|
||||
$getFirstNumeric = function (array $map, array $possibleKeys) {
|
||||
foreach ($possibleKeys as $search) {
|
||||
$normSearch = strtoupper($search);
|
||||
$normSearch = str_replace([' ', '/', '-', '.'], '', $normSearch);
|
||||
|
||||
foreach ($map as $nKey => $value) {
|
||||
if (strpos($nKey, $normSearch) !== false) {
|
||||
if (is_numeric($value)) {
|
||||
return (float)$value;
|
||||
}
|
||||
if (is_string($value) && is_numeric(trim($value))) {
|
||||
return (float)trim($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// 3) read values – QTY vs TTLQTY separately
|
||||
$ctnKeys = ['CTN', 'CTNS'];
|
||||
$qtyKeys = ['QTY', 'PCS', 'PIECES']; // per-carton qty
|
||||
$ttlQtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY']; // total qty
|
||||
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
|
||||
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
|
||||
$amountKeys = ['AMOUNT', 'TTLAMOUNT', 'TOTALAMOUNT'];
|
||||
|
||||
$ctn = $getFirstNumeric($normalizedMap, $ctnKeys);
|
||||
|
||||
// per carton qty
|
||||
$qty = $getFirstNumeric($normalizedMap, $qtyKeys);
|
||||
|
||||
// total qty direct from TOTALQTY/TTLQTY/ITLQTY
|
||||
$ttlQ = $getFirstNumeric($normalizedMap, $ttlQtyKeys);
|
||||
|
||||
// if total column is 0 then compute ctn * qty
|
||||
if ($ttlQ == 0 && $ctn && $qty) {
|
||||
$ttlQ = $ctn * $qty;
|
||||
}
|
||||
|
||||
$cbm = $getFirstNumeric($normalizedMap, ['CBM']);
|
||||
$ttlC = $getFirstNumeric($normalizedMap, ['TOTALCBM', 'TTLCBM', 'ITLCBM']);
|
||||
if ($ttlC == 0 && $cbm && $ctn) {
|
||||
$ttlC = $cbm * $ctn;
|
||||
}
|
||||
|
||||
$kg = $getFirstNumeric($normalizedMap, ['KG', 'WEIGHT']);
|
||||
$ttlK = $getFirstNumeric($normalizedMap, ['TOTALKG', 'TTKG']);
|
||||
if ($ttlK == 0 && $kg && $ctn) {
|
||||
$ttlK = $kg * $ctn;
|
||||
}
|
||||
|
||||
$price = $getFirstNumeric($normalizedMap, ['PRICE', 'RATE']);
|
||||
$amount = $getFirstNumeric($normalizedMap, $amountKeys);
|
||||
if ($amount == 0 && $price && $ttlQ) {
|
||||
$amount = $price * $ttlQ;
|
||||
}
|
||||
|
||||
// 4) get description
|
||||
$desc = null;
|
||||
foreach (['DESCRIPTION', 'DESC'] as $dKey) {
|
||||
$normD = str_replace([' ', '/', '-', '.'], '', strtoupper($dKey));
|
||||
foreach ($normalizedMap as $nKey => $v) {
|
||||
if (strpos($nKey, $normD) !== false) {
|
||||
$desc = is_string($v) ? trim($v) : $v;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rowIndex = $row->row_index;
|
||||
|
||||
// 5) find linked invoice_items
|
||||
$items = InvoiceItem::where('container_id', $container->id)
|
||||
->where('container_row_index', $rowIndex)
|
||||
->get();
|
||||
|
||||
if ($items->isEmpty() && $desc) {
|
||||
$items = InvoiceItem::where('container_id', $container->id)
|
||||
->whereNull('container_row_index')
|
||||
->where('description', $desc)
|
||||
->get();
|
||||
}
|
||||
|
||||
// 6) update invoice_items + recalc invoice totals
|
||||
foreach ($items as $item) {
|
||||
$item->description = $desc;
|
||||
$item->ctn = $ctn;
|
||||
$item->qty = $qty; // per carton
|
||||
$item->ttl_qty = $ttlQ; // total
|
||||
$item->price = $price;
|
||||
$item->ttl_amount = $amount;
|
||||
$item->cbm = $cbm;
|
||||
$item->ttl_cbm = $ttlC;
|
||||
$item->kg = $kg;
|
||||
$item->ttl_kg = $ttlK;
|
||||
$item->save();
|
||||
|
||||
$invoice = $item->invoice;
|
||||
if ($invoice) {
|
||||
$newBaseAmount = InvoiceItem::where('invoice_id', $invoice->id)
|
||||
->sum('ttl_amount');
|
||||
|
||||
$taxType = $invoice->tax_type;
|
||||
$cgstPercent = (float) ($invoice->cgst_percent ?? 0);
|
||||
$sgstPercent = (float) ($invoice->sgst_percent ?? 0);
|
||||
$igstPercent = (float) ($invoice->igst_percent ?? 0);
|
||||
|
||||
$gstPercent = 0;
|
||||
if ($taxType === 'gst') {
|
||||
$gstPercent = $cgstPercent + $sgstPercent;
|
||||
} elseif ($taxType === 'igst') {
|
||||
$gstPercent = $igstPercent;
|
||||
}
|
||||
|
||||
$gstAmount = $newBaseAmount * $gstPercent / 100;
|
||||
$finalWithGst = $newBaseAmount + $gstAmount;
|
||||
|
||||
$invoice->final_amount = $newBaseAmount;
|
||||
$invoice->gst_amount = $gstAmount;
|
||||
$invoice->final_amount_with_gst = $finalWithGst;
|
||||
$invoice->gst_percent = $gstPercent;
|
||||
$invoice->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('containers.show', $container->id)
|
||||
->with('success', 'Excel rows updated successfully.');
|
||||
}
|
||||
|
||||
public function updateStatus(Request $request, Container $container)
|
||||
{
|
||||
$request->validate([
|
||||
'status' => 'required|in:container-ready,export-custom,international-transit,arrived-at-india,import-custom,warehouse,domestic-distribution,out-for-delivery,delivered',
|
||||
]);
|
||||
|
||||
$container->status = $request->status;
|
||||
$container->save();
|
||||
|
||||
if ($request->wantsJson() || $request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'status' => $container->status,
|
||||
]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Container status updated.');
|
||||
}
|
||||
|
||||
public function destroy(Container $container)
|
||||
{
|
||||
$container->delete();
|
||||
|
||||
if (request()->wantsJson() || request()->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Container deleted',
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('containers.index')
|
||||
->with('success', 'Container deleted.');
|
||||
}
|
||||
|
||||
private function generateInvoiceNumber(): string
|
||||
{
|
||||
$year = now()->format('Y');
|
||||
|
||||
$last = Invoice::whereYear('created_at', $year)
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
if ($last) {
|
||||
$parts = explode('-', $last->invoice_number);
|
||||
$seq = 0;
|
||||
|
||||
if (count($parts) === 3) {
|
||||
$seq = (int) $parts[2];
|
||||
}
|
||||
|
||||
$nextSeq = $seq + 1;
|
||||
} else {
|
||||
$nextSeq = 1;
|
||||
}
|
||||
|
||||
return 'INV-' . $year . '-' . str_pad($nextSeq, 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
public function downloadPdf(Container $container)
|
||||
{
|
||||
$container->load('rows');
|
||||
|
||||
$pdf = Pdf::loadView('admin.container_pdf', [
|
||||
'container' => $container,
|
||||
])->setPaper('a4', 'landscape');
|
||||
|
||||
$fileName = 'container-'.$container->container_number.'.pdf';
|
||||
|
||||
return $pdf->download($fileName);
|
||||
}
|
||||
|
||||
public function downloadExcel(Container $container)
|
||||
{
|
||||
if (!$container->excel_file) {
|
||||
abort(404, 'Excel file not found on record.');
|
||||
}
|
||||
|
||||
// Stored path like "containers/abc.xlsx"
|
||||
$path = $container->excel_file;
|
||||
|
||||
if (!Storage::exists($path)) {
|
||||
abort(404, 'Excel file missing on server.');
|
||||
}
|
||||
|
||||
$fileName = 'container-'.$container->container_number.'.xlsx';
|
||||
|
||||
return Storage::download($path, $fileName);
|
||||
}
|
||||
|
||||
|
||||
public function popupPopup(Container $container)
|
||||
{
|
||||
// existing show सारखाच data वापरू
|
||||
$container->load('rows');
|
||||
|
||||
// summary आधीपासून index() मध्ये जसा काढतोस तसाच logic reuse
|
||||
$rows = $container->rows ?? collect();
|
||||
|
||||
$totalCtn = 0;
|
||||
$totalQty = 0;
|
||||
$totalCbm = 0;
|
||||
$totalKg = 0;
|
||||
|
||||
$ctnKeys = ['CTN', 'CTNS'];
|
||||
$qtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY', 'QTY', 'PCS', 'PIECES'];
|
||||
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
|
||||
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
|
||||
|
||||
$getFirstNumeric = function (array $data, array $possibleKeys) {
|
||||
$normalizedMap = [];
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key === null) continue;
|
||||
$normKey = strtoupper((string)$key);
|
||||
$normKey = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normKey);
|
||||
$normalizedMap[$normKey] = $value;
|
||||
}
|
||||
|
||||
foreach ($possibleKeys as $search) {
|
||||
$normSearch = strtoupper($search);
|
||||
$normSearch = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normSearch);
|
||||
|
||||
foreach ($normalizedMap as $nKey => $value) {
|
||||
if (strpos($nKey, $normSearch) !== false) {
|
||||
if (is_numeric($value)) {
|
||||
return (float)$value;
|
||||
}
|
||||
if (is_string($value) && is_numeric(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);
|
||||
}
|
||||
|
||||
$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,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,76 +6,48 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class UserAuthController extends Controller
|
||||
{
|
||||
|
||||
public function refreshToken()
|
||||
{
|
||||
\Log::info('🔄 refreshToken() called');
|
||||
public function refreshToken()
|
||||
{
|
||||
Log::info('🔄 [JWT-REFRESH] called');
|
||||
|
||||
try {
|
||||
// Get current token
|
||||
$currentToken = JWTAuth::getToken();
|
||||
try {
|
||||
$newToken = JWTAuth::parseToken()->refresh();
|
||||
|
||||
if (!$currentToken) {
|
||||
\Log::warning('⚠ No token provided in refreshToken()');
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Token not provided',
|
||||
], 401);
|
||||
}
|
||||
Log::info('✅ [JWT-REFRESH] Token refreshed');
|
||||
|
||||
\Log::info('📥 Current Token:', ['token' => (string) $currentToken]);
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'token' => $newToken,
|
||||
]);
|
||||
|
||||
// Try refreshing token
|
||||
$newToken = JWTAuth::refresh($currentToken);
|
||||
} catch (\PHPOpenSourceSaver\JWTAuth\Exceptions\TokenExpiredException $e) {
|
||||
Log::warning('⛔ [JWT-REFRESH] Refresh TTL expired');
|
||||
|
||||
\Log::info('✅ Token refreshed successfully', ['new_token' => $newToken]);
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Refresh expired. Please login again.',
|
||||
], 401);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'token' => $newToken,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('🔥 [JWT-REFRESH] Exception', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
} catch (\Tymon\JWTAuth\Exceptions\TokenExpiredException $e) {
|
||||
\Log::error('❌ TokenExpiredException in refreshToken()', [
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Token expired, cannot refresh.',
|
||||
], 401);
|
||||
|
||||
} catch (\Tymon\JWTAuth\Exceptions\TokenInvalidException $e) {
|
||||
\Log::error('❌ TokenInvalidException in refreshToken()', [
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Invalid token.',
|
||||
], 401);
|
||||
|
||||
} catch (\Tymon\JWTAuth\Exceptions\JWTException $e) {
|
||||
\Log::error('❌ JWTException in refreshToken()', [
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Could not refresh token.',
|
||||
], 401);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('❌ General Exception in refreshToken()', [
|
||||
'message' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unexpected error while refreshing token.',
|
||||
], 500);
|
||||
}
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unable to refresh token.',
|
||||
], 401);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* User Login
|
||||
|
||||
96
app/Http/Controllers/user/ChatController.php
Normal file
96
app/Http/Controllers/user/ChatController.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\SupportTicket;
|
||||
use App\Models\ChatMessage;
|
||||
use App\Events\NewChatMessage;
|
||||
|
||||
class ChatController extends Controller
|
||||
{
|
||||
/**
|
||||
* Start chat or return existing ticket for this user
|
||||
*/
|
||||
public function startChat()
|
||||
{
|
||||
// One chat ticket per user
|
||||
$ticket = SupportTicket::firstOrCreate([
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'ticket' => $ticket
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all messages for this ticket
|
||||
*/
|
||||
public function getMessages($ticketId)
|
||||
{
|
||||
// Ensure this ticket belongs to the logged-in user
|
||||
$ticket = SupportTicket::where('id', $ticketId)
|
||||
->where('user_id', auth()->id())
|
||||
->firstOrFail();
|
||||
|
||||
$messages = ChatMessage::where('ticket_id', $ticketId)
|
||||
->orderBy('created_at', 'asc')
|
||||
->with('sender')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'messages' => $messages
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send text or file message from user → admin/staff
|
||||
*/
|
||||
public function sendMessage(Request $request, $ticketId)
|
||||
{
|
||||
$request->validate([
|
||||
'message' => 'nullable|string',
|
||||
'file' => 'nullable|file|max:20480', // 20MB limit
|
||||
]);
|
||||
|
||||
// Validate ticket ownership
|
||||
$ticket = SupportTicket::where('id', $ticketId)
|
||||
->where('user_id', auth()->id())
|
||||
->firstOrFail();
|
||||
|
||||
$data = [
|
||||
'ticket_id' => $ticketId,
|
||||
'sender_id' => auth()->id(),
|
||||
'sender_type' => \App\Models\User::class,
|
||||
'message' => $request->message,
|
||||
'client_id' => $request->client_id, // ✅ ADD
|
||||
'read_by_admin' => false,
|
||||
'read_by_user' => true,
|
||||
];
|
||||
|
||||
// Handle file upload
|
||||
if ($request->hasFile('file')) {
|
||||
$path = $request->file('file')->store('chat', 'public');
|
||||
$data['file_path'] = $path;
|
||||
$data['file_type'] = $request->file('file')->getMimeType();
|
||||
}
|
||||
|
||||
// Save message
|
||||
$message = ChatMessage::create($data);
|
||||
|
||||
// Load sender info for broadcast
|
||||
$message->load('sender');
|
||||
|
||||
// Fire real-time event
|
||||
broadcast(new NewChatMessage($message));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -289,6 +289,44 @@ public function invoiceDetails($invoice_id)
|
||||
]);
|
||||
}
|
||||
|
||||
public function confirmOrder($order_id)
|
||||
{
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
if (! $user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 401);
|
||||
}
|
||||
|
||||
$order = $user->orders()
|
||||
->where('order_id', $order_id)
|
||||
->first();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
$order->status = 'order_confirmed';
|
||||
$order->save();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Order confirmed successfully'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ use Tymon\JWTAuth\Exceptions\JWTException;
|
||||
class JwtRefreshMiddleware
|
||||
{
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
{
|
||||
|
||||
try {
|
||||
JWTAuth::parseToken()->authenticate();
|
||||
} catch (TokenExpiredException $e) {
|
||||
|
||||
26
app/Imports/OrderItemsPreviewImport.php
Normal file
26
app/Imports/OrderItemsPreviewImport.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace App\Imports;
|
||||
|
||||
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class OrderItemsPreviewImport implements ToCollection
|
||||
{
|
||||
public array $rows = [];
|
||||
|
||||
public function collection(Collection $collection)
|
||||
{
|
||||
$header = $collection->first()->map(fn ($h) => strtolower(trim($h)))->toArray();
|
||||
|
||||
foreach ($collection->skip(1) as $row) {
|
||||
$item = [];
|
||||
foreach ($header as $i => $key) {
|
||||
$item[$key] = $row[$i] ?? null;
|
||||
}
|
||||
|
||||
if (!empty($item['description'])) {
|
||||
$this->rows[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ class Admin extends Authenticatable
|
||||
'name', 'email', 'password', 'username',
|
||||
'phone', 'emergency_phone', 'address',
|
||||
'role', 'department', 'designation', 'joining_date',
|
||||
'status', 'additional_info', 'type', // admin/staff indicator
|
||||
'status', 'additional_info', 'type','employee_id', // admin/staff indicator
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
|
||||
38
app/Models/ChatMessage.php
Normal file
38
app/Models/ChatMessage.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class ChatMessage extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'ticket_id',
|
||||
'sender_id',
|
||||
'sender_type',
|
||||
'message',
|
||||
'file_path',
|
||||
'file_type',
|
||||
'read_by_admin',
|
||||
'read_by_user',
|
||||
'client_id',
|
||||
];
|
||||
/**
|
||||
* The ticket this message belongs to.
|
||||
*/
|
||||
public function ticket()
|
||||
{
|
||||
return $this->belongsTo(SupportTicket::class, 'ticket_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Polymorphic sender (User or Admin)
|
||||
*/
|
||||
public function sender()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
30
app/Models/Container.php
Normal file
30
app/Models/Container.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Container extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'container_name',
|
||||
'container_number',
|
||||
'container_date',
|
||||
'status',
|
||||
'excel_file',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'container_date' => 'date',
|
||||
];
|
||||
|
||||
public function rows()
|
||||
{
|
||||
return $this->hasMany(ContainerRow::class);
|
||||
}
|
||||
|
||||
public function invoices()
|
||||
{
|
||||
return $this->hasMany(Invoice::class);
|
||||
}
|
||||
}
|
||||
23
app/Models/ContainerRow.php
Normal file
23
app/Models/ContainerRow.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ContainerRow extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'container_id',
|
||||
'row_index',
|
||||
'data',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'data' => 'array',
|
||||
];
|
||||
|
||||
public function container()
|
||||
{
|
||||
return $this->belongsTo(Container::class);
|
||||
}
|
||||
}
|
||||
@@ -9,41 +9,29 @@ class Invoice extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'order_id',
|
||||
'customer_id',
|
||||
'mark_no',
|
||||
|
||||
'invoice_number',
|
||||
'invoice_date',
|
||||
'due_date',
|
||||
|
||||
'payment_method',
|
||||
'reference_no',
|
||||
'status',
|
||||
|
||||
'final_amount', // 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',
|
||||
'company_name',
|
||||
'customer_email',
|
||||
'customer_mobile',
|
||||
'customer_address',
|
||||
'pincode',
|
||||
|
||||
'pdf_path',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'container_id',
|
||||
'customer_id',
|
||||
'mark_no',
|
||||
'invoice_number',
|
||||
'invoice_date',
|
||||
'due_date',
|
||||
'payment_method',
|
||||
'reference_no',
|
||||
'status',
|
||||
'final_amount',
|
||||
'gst_percent',
|
||||
'gst_amount',
|
||||
'final_amount_with_gst',
|
||||
'customer_name',
|
||||
'company_name',
|
||||
'customer_email',
|
||||
'customer_mobile',
|
||||
'customer_address',
|
||||
'pincode',
|
||||
'pdf_path',
|
||||
'notes',
|
||||
];
|
||||
|
||||
/****************************
|
||||
* Relationships
|
||||
@@ -54,9 +42,9 @@ class Invoice extends Model
|
||||
return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC');
|
||||
}
|
||||
|
||||
public function order()
|
||||
public function container()
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
return $this->belongsTo(Container::class);
|
||||
}
|
||||
|
||||
public function customer()
|
||||
@@ -64,19 +52,28 @@ class Invoice extends Model
|
||||
return $this->belongsTo(User::class, 'customer_id');
|
||||
}
|
||||
|
||||
public function installments()
|
||||
{
|
||||
return $this->hasMany(InvoiceInstallment::class);
|
||||
}
|
||||
|
||||
// ✅ SINGLE, correct relation
|
||||
public function chargeGroups()
|
||||
{
|
||||
return $this->hasMany(\App\Models\InvoiceChargeGroup::class, 'invoice_id');
|
||||
}
|
||||
|
||||
/****************************
|
||||
* Helper Functions
|
||||
****************************/
|
||||
|
||||
// Auto calculate GST fields (you can call this in controller before saving)
|
||||
public function calculateTotals()
|
||||
{
|
||||
$gst = ($this->final_amount * $this->gst_percent) / 100;
|
||||
$this->gst_amount = $gst;
|
||||
$this->gst_amount = $gst;
|
||||
$this->final_amount_with_gst = $this->final_amount + $gst;
|
||||
}
|
||||
|
||||
// Check overdue status condition
|
||||
public function isOverdue()
|
||||
{
|
||||
return $this->status === 'pending' && now()->gt($this->due_date);
|
||||
@@ -84,12 +81,31 @@ class Invoice extends Model
|
||||
|
||||
public function getShipment()
|
||||
{
|
||||
return $this->order?->shipments?->first();
|
||||
return null;
|
||||
}
|
||||
|
||||
public function installments()
|
||||
// ✅ Charge groups total accessor
|
||||
public function getChargeGroupsTotalAttribute()
|
||||
{
|
||||
// relation already loaded असेल तर collection वरून sum होईल
|
||||
return (float) $this->chargeGroups->sum('total_charge');
|
||||
}
|
||||
|
||||
// ✅ Grand total accessor (items + GST + charge groups)
|
||||
public function getGrandTotalWithChargesAttribute()
|
||||
{
|
||||
return (float) ($this->final_amount_with_gst ?? 0) + $this->charge_groups_total;
|
||||
}
|
||||
|
||||
public function totalPaid()
|
||||
{
|
||||
return $this->hasMany(InvoiceInstallment::class);
|
||||
return $this->installments->sum('amount');
|
||||
}
|
||||
|
||||
|
||||
public function remainingAmount()
|
||||
{
|
||||
return $this->grand_total_with_charges - $this->totalPaid();
|
||||
}
|
||||
|
||||
|
||||
|
||||
27
app/Models/InvoiceChargeGroup.php
Normal file
27
app/Models/InvoiceChargeGroup.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class InvoiceChargeGroup extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'invoice_id',
|
||||
'group_name',
|
||||
'basis_type',
|
||||
'basis_value',
|
||||
'rate',
|
||||
'total_charge',
|
||||
];
|
||||
|
||||
public function invoice()
|
||||
{
|
||||
return $this->belongsTo(Invoice::class);
|
||||
}
|
||||
|
||||
public function items()
|
||||
{
|
||||
return $this->hasMany(InvoiceChargeGroupItem::class, 'group_id');
|
||||
}
|
||||
}
|
||||
23
app/Models/InvoiceChargeGroupItem.php
Normal file
23
app/Models/InvoiceChargeGroupItem.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class InvoiceChargeGroupItem extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'group_id',
|
||||
'invoice_item_id',
|
||||
];
|
||||
|
||||
public function group()
|
||||
{
|
||||
return $this->belongsTo(InvoiceChargeGroup::class, 'group_id');
|
||||
}
|
||||
|
||||
public function item()
|
||||
{
|
||||
return $this->belongsTo(InvoiceItem::class, 'invoice_item_id');
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ class InvoiceItem extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'invoice_id',
|
||||
'container_id', // Container mapping
|
||||
'container_row_index', // Container row index
|
||||
|
||||
'description',
|
||||
'ctn',
|
||||
@@ -37,4 +39,79 @@ class InvoiceItem extends Model
|
||||
{
|
||||
return $this->belongsTo(Invoice::class);
|
||||
}
|
||||
|
||||
public function chargeGroupItems()
|
||||
{
|
||||
return $this->hasMany(InvoiceChargeGroupItem::class, 'invoice_item_id');
|
||||
}
|
||||
|
||||
// हे helper: पहिला group fetch करून त्यावरून rate/total काढणे
|
||||
public function getChargeRateAttribute()
|
||||
{
|
||||
$pivot = $this->chargeGroupItems->first();
|
||||
if (!$pivot || !$pivot->group) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$group = $pivot->group;
|
||||
|
||||
// basis नुसार या item चा basis value
|
||||
$basis = 0;
|
||||
switch ($group->basis_type) {
|
||||
case 'ttl_qty':
|
||||
$basis = $this->ttl_qty;
|
||||
break;
|
||||
case 'amount':
|
||||
$basis = $this->ttl_amount;
|
||||
break;
|
||||
case 'ttl_cbm':
|
||||
$basis = $this->ttl_cbm;
|
||||
break;
|
||||
case 'ttl_kg':
|
||||
$basis = $this->ttl_kg;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($basis <= 0 || ($group->basis_value ?? 0) <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// group चा rate field आधीच आहे, ते direct वापरू
|
||||
return (float) $group->rate;
|
||||
}
|
||||
|
||||
public function getChargeTotalAttribute()
|
||||
{
|
||||
$pivot = $this->chargeGroupItems->first();
|
||||
if (!$pivot || !$pivot->group) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$group = $pivot->group;
|
||||
|
||||
$basis = 0;
|
||||
switch ($group->basis_type) {
|
||||
case 'ttl_qty':
|
||||
$basis = $this->ttl_qty;
|
||||
break;
|
||||
case 'amount':
|
||||
$basis = $this->ttl_amount;
|
||||
break;
|
||||
case 'ttl_cbm':
|
||||
$basis = $this->ttl_cbm;
|
||||
break;
|
||||
case 'ttl_kg':
|
||||
$basis = $this->ttl_kg;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($basis <= 0 || ($group->basis_value ?? 0) <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// per unit rate
|
||||
$rate = (float) $group->rate;
|
||||
// item total = basis * rate
|
||||
return $basis * $rate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,11 +58,31 @@ class Order extends Model
|
||||
return $this->belongsToMany(\App\Models\Shipment::class, 'shipment_items', 'order_id', 'shipment_id');
|
||||
}
|
||||
|
||||
public function invoice()
|
||||
// public function invoice()
|
||||
// {
|
||||
// return $this->hasOne(\App\Models\Invoice::class, 'order_id', 'id');
|
||||
// }
|
||||
|
||||
|
||||
const STATUS_LABELS = [
|
||||
'order_placed' => 'Order Placed',
|
||||
'order_confirmed' => 'Order Confirmed',
|
||||
'supplier_warehouse' => 'Supplier Warehouse',
|
||||
'consolidate_warehouse'=> 'Consolidate Warehouse',
|
||||
'export_custom' => 'Export Custom',
|
||||
'international_transit'=> 'International Transit',
|
||||
'arrived_india' => 'Arrived at India',
|
||||
'import_custom' => 'Import Custom',
|
||||
'warehouse' => 'Warehouse',
|
||||
'domestic_distribution'=> 'Domestic Distribution',
|
||||
'out_for_delivery' => 'Out for Delivery',
|
||||
'delivered' => 'Delivered',
|
||||
];
|
||||
|
||||
public function getStatusLabelAttribute()
|
||||
{
|
||||
return $this->hasOne(\App\Models\Invoice::class, 'order_id', 'id');
|
||||
return self::STATUS_LABELS[$this->status]
|
||||
?? ucfirst(str_replace('_', ' ', $this->status));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -45,25 +45,6 @@ class Shipment extends Model
|
||||
return $this->belongsToMany(Order::class, 'shipment_items', 'shipment_id', 'order_id');
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// STATUS CONSTANTS
|
||||
// ---------------------------
|
||||
|
||||
const STATUS_PENDING = 'pending';
|
||||
const STATUS_IN_TRANSIT = 'in_transit';
|
||||
const STATUS_DISPATCHED = 'dispatched';
|
||||
const STATUS_DELIVERED = 'delivered';
|
||||
|
||||
public static function statusOptions()
|
||||
{
|
||||
return [
|
||||
self::STATUS_PENDING => 'Pending',
|
||||
self::STATUS_IN_TRANSIT => 'In Transit',
|
||||
self::STATUS_DISPATCHED => 'Dispatched',
|
||||
self::STATUS_DELIVERED => 'Delivered',
|
||||
];
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// HELPERS
|
||||
// ---------------------------
|
||||
@@ -73,8 +54,38 @@ class Shipment extends Model
|
||||
return $this->items()->count();
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// STATUS CONSTANTS (LOGISTICS FLOW)
|
||||
// ---------------------------
|
||||
const STATUS_SHIPMENT_READY = 'shipment_ready';
|
||||
const STATUS_EXPORT_CUSTOM = 'export_custom';
|
||||
const STATUS_INTERNATIONAL_TRANSIT= 'international_transit';
|
||||
const STATUS_ARRIVED_INDIA = 'arrived_india';
|
||||
const STATUS_IMPORT_CUSTOM = 'import_custom';
|
||||
const STATUS_WAREHOUSE = 'warehouse';
|
||||
const STATUS_DOMESTIC_DISTRIBUTION= 'domestic_distribution';
|
||||
const STATUS_OUT_FOR_DELIVERY = 'out_for_delivery';
|
||||
const STATUS_DELIVERED = 'delivered';
|
||||
|
||||
public static function statusOptions()
|
||||
{
|
||||
return [
|
||||
self::STATUS_SHIPMENT_READY => 'Shipment Ready',
|
||||
self::STATUS_EXPORT_CUSTOM => 'Export Custom',
|
||||
self::STATUS_INTERNATIONAL_TRANSIT => 'International Transit',
|
||||
self::STATUS_ARRIVED_INDIA => 'Arrived at India',
|
||||
self::STATUS_IMPORT_CUSTOM => 'Import Custom',
|
||||
self::STATUS_WAREHOUSE => 'Warehouse',
|
||||
self::STATUS_DOMESTIC_DISTRIBUTION => 'Domestic Distribution',
|
||||
self::STATUS_OUT_FOR_DELIVERY => 'Out for Delivery',
|
||||
self::STATUS_DELIVERED => 'Delivered',
|
||||
];
|
||||
}
|
||||
|
||||
public function statusLabel()
|
||||
{
|
||||
return self::statusOptions()[$this->status] ?? ucfirst($this->status);
|
||||
return self::statusOptions()[$this->status]
|
||||
?? ucfirst(str_replace('_', ' ', $this->status));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
32
app/Models/SupportTicket.php
Normal file
32
app/Models/SupportTicket.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class SupportTicket extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'status',
|
||||
];
|
||||
|
||||
/**
|
||||
* The user (customer) who owns this ticket.
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* All chat messages for this ticket.
|
||||
*/
|
||||
public function messages()
|
||||
{
|
||||
return $this->hasMany(ChatMessage::class, 'ticket_id')->orderBy('created_at', 'asc');
|
||||
}
|
||||
}
|
||||
@@ -89,9 +89,23 @@ class User extends Authenticatable implements JWTSubject
|
||||
{
|
||||
return [];
|
||||
}
|
||||
public function invoices()
|
||||
public function invoices()
|
||||
{
|
||||
return $this->hasMany(\App\Models\Invoice::class, 'customer_id', 'customer_id');
|
||||
}
|
||||
|
||||
|
||||
// App\Models\User.php
|
||||
|
||||
|
||||
public function invoiceInstallments()
|
||||
{
|
||||
return $this->hasMany(\App\Models\Invoice::class, 'customer_id', 'id');
|
||||
return $this->hasManyThrough(
|
||||
InvoiceInstallment::class,
|
||||
Invoice::class,
|
||||
'customer_id', // FK on invoices
|
||||
'invoice_id' // FK on installments
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
22
app/Providers/BroadcastServiceProvider.php
Normal file
22
app/Providers/BroadcastServiceProvider.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class BroadcastServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
Broadcast::routes([
|
||||
'middleware' => ['web'],
|
||||
]);
|
||||
|
||||
// 👇 FORCE admin guard for broadcasting
|
||||
Auth::shouldUse('admin');
|
||||
|
||||
require base_path('routes/channels.php');
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,18 @@ use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
web: [
|
||||
__DIR__.'/../routes/web.php',
|
||||
__DIR__.'/../routes/channels.php',
|
||||
],
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
//
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
//
|
||||
})->create();
|
||||
})
|
||||
->create();
|
||||
|
||||
@@ -4,4 +4,6 @@ return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\AuthServiceProvider::class,
|
||||
App\Providers\BroadcastServiceProvider::class,
|
||||
|
||||
];
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
"php": "^8.2",
|
||||
"barryvdh/laravel-dompdf": "^3.1",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/reverb": "^1.6",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"maatwebsite/excel": "^1.1",
|
||||
"maatwebsite/excel": "^3.1",
|
||||
"mpdf/mpdf": "^8.2",
|
||||
"php-open-source-saver/jwt-auth": "2.8",
|
||||
"spatie/laravel-permission": "^6.23"
|
||||
|
||||
1561
composer.lock
generated
1561
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -89,6 +89,8 @@ return [
|
||||
'driver' => 'eloquent',
|
||||
'model' => App\Models\Staff::class,
|
||||
],
|
||||
|
||||
|
||||
|
||||
],
|
||||
|
||||
|
||||
31
config/broadcasting.php
Normal file
31
config/broadcasting.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'default' => env('BROADCAST_DRIVER', 'null'),
|
||||
|
||||
'connections' => [
|
||||
|
||||
'reverb' => [
|
||||
'driver' => 'reverb',
|
||||
'key' => env('REVERB_APP_KEY'),
|
||||
'secret' => env('REVERB_APP_SECRET'),
|
||||
'app_id' => env('REVERB_APP_ID'),
|
||||
'options' => [
|
||||
'host' => env('REVERB_HOST'),
|
||||
'port' => env('REVERB_PORT'),
|
||||
'scheme' => env('REVERB_SCHEME'),
|
||||
'useTLS' => false,
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
'log' => [
|
||||
'driver' => 'log',
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'null',
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -89,7 +89,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'ttl' => (int) env('JWT_TTL', 60),
|
||||
'ttl' => (int) env('JWT_TTL', 1440),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -108,7 +108,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'refresh_ttl' => (int) env('JWT_REFRESH_TTL', 20160),
|
||||
'refresh_ttl' => (int) env('JWT_REFRESH_TTL', 64800),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
96
config/reverb.php
Normal file
96
config/reverb.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Reverb Server
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'default' => env('REVERB_SERVER', 'reverb'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reverb Servers
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'servers' => [
|
||||
|
||||
'reverb' => [
|
||||
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'), // WebSocket listens here
|
||||
'port' => env('REVERB_SERVER_PORT', 8080), // WebSocket port
|
||||
'path' => env('REVERB_SERVER_PATH', ''),
|
||||
|
||||
// Used for Echo client hostname
|
||||
'hostname' => env('REVERB_HOST', 'localhost'),
|
||||
|
||||
'options' => [
|
||||
'tls' => [], // No TLS for localhost
|
||||
],
|
||||
|
||||
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10000),
|
||||
|
||||
'scaling' => [
|
||||
'enabled' => env('REVERB_SCALING_ENABLED', false),
|
||||
],
|
||||
|
||||
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
|
||||
'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reverb Applications
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'apps' => [
|
||||
|
||||
'provider' => 'config',
|
||||
|
||||
'apps' => [
|
||||
[
|
||||
'key' => env('REVERB_APP_KEY'),
|
||||
'secret' => env('REVERB_APP_SECRET'),
|
||||
'app_id' => env('REVERB_APP_ID'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Echo + Flutter Client Options
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'options' => [
|
||||
'host' => env('REVERB_HOST', 'localhost'), // for client connections
|
||||
'port' => env('REVERB_PORT', 8080), // SAME as WebSocket server port
|
||||
'scheme' => env('REVERB_SCHEME', 'http'),
|
||||
'useTLS' => false,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Allowed Origins (Important)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| "*" allows all origins:
|
||||
| - Flutter (Android/iOS/Web)
|
||||
| - Admin Panel
|
||||
| - Localhost
|
||||
|
|
||||
*/
|
||||
'allowed_origins' => ['*'],
|
||||
|
||||
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
|
||||
'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
|
||||
'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'),
|
||||
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10000),
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -33,6 +33,9 @@ class CreateInvoiceItemsTable extends Migration
|
||||
$table->string('shop_no')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unsignedBigInteger('container_id')->nullable()->after('invoice_id');
|
||||
$table->integer('container_row_index')->nullable()->after('container_id');
|
||||
|
||||
// FK
|
||||
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');
|
||||
@@ -49,4 +52,6 @@ class CreateInvoiceItemsTable extends Migration
|
||||
});
|
||||
Schema::dropIfExists('invoice_items');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -13,16 +13,28 @@ return new class extends Migration
|
||||
{
|
||||
Schema::create('chat_messages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('ticket_id'); // support ticket ID
|
||||
$table->unsignedBigInteger('sender_id'); // user or admin/staff
|
||||
$table->text('message')->nullable(); // message content
|
||||
$table->string('file_path')->nullable(); // image/pdf/video
|
||||
$table->string('file_type')->default('text'); // text/image/pdf/video
|
||||
|
||||
// Chat belongs to a ticket
|
||||
$table->unsignedBigInteger('ticket_id');
|
||||
|
||||
// POLYMORPHIC sender (User OR Admin)
|
||||
$table->unsignedBigInteger('sender_id');
|
||||
$table->string('sender_type');
|
||||
// Example values:
|
||||
// - "App\Models\User"
|
||||
// - "App\Models\Admin"
|
||||
|
||||
// Content
|
||||
$table->text('message')->nullable();
|
||||
$table->string('file_path')->nullable(); // storage/app/public/chat/...
|
||||
$table->string('file_type')->default('text'); // text / image / video / pdf
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// foreign keys
|
||||
$table->foreign('ticket_id')->references('id')->on('support_tickets')->onDelete('cascade');
|
||||
$table->foreign('sender_id')->references('id')->on('users')->onDelete('cascade'); // admin also stored in users table? If admin separate, change later.
|
||||
// FK to tickets table
|
||||
$table->foreign('ticket_id')
|
||||
->references('id')->on('support_tickets')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('chat_messages', function (Blueprint $table) {
|
||||
$table->boolean('read_by_admin')->default(false)->after('file_type');
|
||||
$table->boolean('read_by_user')->default(false)->after('read_by_admin');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('chat_messages', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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('chat_messages', function (Blueprint $table) {
|
||||
$table->string('client_id')
|
||||
->nullable()
|
||||
->after('sender_type')
|
||||
->index();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('chat_messages', function (Blueprint $table) {
|
||||
$table->dropColumn('client_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('containers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('container_name');
|
||||
$table->string('container_number')->unique();
|
||||
$table->date('container_date');
|
||||
$table->string('excel_file')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('containers');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('loading_list_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('container_id')
|
||||
->constrained('containers')
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->string('mark')->nullable(); // MARK / ITEM NO
|
||||
$table->string('description')->nullable();
|
||||
$table->integer('ctn')->nullable();
|
||||
$table->integer('qty')->nullable();
|
||||
$table->integer('total_qty')->nullable();
|
||||
$table->string('unit')->nullable();
|
||||
$table->decimal('price', 15, 3)->nullable(); // SAHIL format साठी
|
||||
$table->decimal('cbm', 15, 5)->nullable();
|
||||
$table->decimal('total_cbm', 15, 5)->nullable();
|
||||
$table->decimal('kg', 15, 3)->nullable();
|
||||
$table->decimal('total_kg', 15, 3)->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('loading_list_items');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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('container_rows', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('container_id')
|
||||
->constrained('containers')
|
||||
->onDelete('cascade');
|
||||
|
||||
// Excel मधल्या row क्रमांकासाठी (optional)
|
||||
$table->unsignedInteger('row_index')->nullable();
|
||||
|
||||
// या row चा full data: "heading text" => "cell value"
|
||||
$table->json('data');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('container_rows');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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('containers', function (Blueprint $table) {
|
||||
$table->string('status', 21)
|
||||
->default('pending')
|
||||
->after('container_date');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('containers', function (Blueprint $table) {
|
||||
$table->dropColumn('status');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
<?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) {
|
||||
// 1) order_id foreign key काढा
|
||||
$table->dropForeign(['order_id']);
|
||||
|
||||
// 2) order_id column काढा
|
||||
$table->dropColumn('order_id');
|
||||
|
||||
// 3) container_id add करा
|
||||
$table->unsignedBigInteger('container_id')->nullable()->after('id');
|
||||
|
||||
// 4) container_id FK
|
||||
$table->foreign('container_id')
|
||||
->references('id')
|
||||
->on('containers')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
// rollback: container_id काढून order_id परत add
|
||||
$table->dropForeign(['container_id']);
|
||||
$table->dropColumn('container_id');
|
||||
|
||||
$table->unsignedBigInteger('order_id')->index();
|
||||
$table->foreign('order_id')
|
||||
->references('id')
|
||||
->on('orders')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('invoice_charge_groups', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('invoice_id');
|
||||
$table->string('group_name')->nullable(); // उदा. "FREIGHT", "HANDLING"
|
||||
$table->enum('basis_type', ['ttl_qty', 'amount', 'ttl_cbm', 'ttl_kg']);
|
||||
$table->decimal('basis_value', 15, 3)->default(0); // auto calculate केलेला total basis
|
||||
$table->decimal('rate', 15, 3)->default(0); // per basis rate (helper)
|
||||
$table->decimal('total_charge', 15, 2); // admin नी manually टाकलेला total
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('invoice_id')
|
||||
->references('id')->on('invoices')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('invoice_charge_groups');
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('invoice_charge_group_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('group_id');
|
||||
$table->unsignedBigInteger('invoice_item_id');
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('group_id')
|
||||
->references('id')->on('invoice_charge_groups')
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->foreign('invoice_item_id')
|
||||
->references('id')->on('invoice_items')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('invoice_charge_group_items');
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('invoice_items', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('container_id')->nullable()->after('invoice_id');
|
||||
$table->integer('container_row_index')->nullable()->after('container_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('invoice_items', function (Blueprint $table) {
|
||||
$table->dropColumn(['container_id', 'container_row_index']);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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) {
|
||||
// column आधीच आहे का हे check करून, नसेल तरच add करायचा
|
||||
if (!Schema::hasColumn('invoices', 'due_date')) {
|
||||
$table->date('due_date')
|
||||
->nullable()
|
||||
->after('invoice_date');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('invoices', 'due_date')) {
|
||||
$table->dropColumn('due_date');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('containers', function (Blueprint $table) {
|
||||
$table->string('status', 50)->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('containers', function (Blueprint $table) {
|
||||
$table->string('status', 20)->change();
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
@@ -25,6 +25,12 @@ class PermissionSeeder extends Seeder
|
||||
// EXTRA (ORDERS)
|
||||
'orders.view', // you added this separately
|
||||
|
||||
// CONTAINER
|
||||
'container.view',
|
||||
'container.create',
|
||||
'container.update',
|
||||
'container.delete',
|
||||
|
||||
// SHIPMENT
|
||||
'shipment.view',
|
||||
'shipment.create',
|
||||
|
||||
2525
package-lock.json
generated
Normal file
2525
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,5 +13,9 @@
|
||||
"laravel-vite-plugin": "^2.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^7.0.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"laravel-echo": "^2.2.6",
|
||||
"pusher-js": "^8.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000016.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000016.pdf
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000027.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000027.pdf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000031.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000031.pdf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000034.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000034.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000044.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000044.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000046.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000046.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000048.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000048.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000051.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000051.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000110.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000110.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000116.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000116.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000117.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000117.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2026-000134.pdf
Normal file
BIN
public/invoices/invoice-INV-2026-000134.pdf
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
BIN
public/profile_upload/profile_1766120292.jpg
Normal file
BIN
public/profile_upload/profile_1766120292.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@@ -1 +1,6 @@
|
||||
import './bootstrap';
|
||||
import "./bootstrap";
|
||||
|
||||
// VERY IMPORTANT — Load Echo globally
|
||||
import "./echo";
|
||||
|
||||
console.log("[APP] app.js loaded");
|
||||
|
||||
7
resources/js/bootstrap.js
vendored
7
resources/js/bootstrap.js
vendored
@@ -1,4 +1,9 @@
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
axios.defaults.withCredentials = true;
|
||||
axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
|
||||
axios.defaults.headers.common["X-CSRF-TOKEN"] = document.querySelector(
|
||||
'meta[name="csrf-token"]'
|
||||
).content;
|
||||
|
||||
|
||||
30
resources/js/echo.js
Normal file
30
resources/js/echo.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import Echo from "laravel-echo";
|
||||
import Pusher from "pusher-js";
|
||||
|
||||
window.Pusher = Pusher;
|
||||
|
||||
console.log("[ECHO] Initializing Reverb...");
|
||||
|
||||
window.Echo = new Echo({
|
||||
broadcaster: "reverb",
|
||||
key: import.meta.env.VITE_REVERB_APP_KEY,
|
||||
|
||||
wsHost: import.meta.env.VITE_REVERB_HOST,
|
||||
wsPort: Number(import.meta.env.VITE_REVERB_PORT),
|
||||
|
||||
forceTLS: false,
|
||||
disableStats: true,
|
||||
|
||||
authEndpoint: "/broadcasting/auth",
|
||||
|
||||
auth: {
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": document
|
||||
.querySelector('meta[name="csrf-token"]')
|
||||
?.getAttribute("content"),
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log("[ECHO] Loaded Successfully!", window.Echo);
|
||||
@@ -0,0 +1,102 @@
|
||||
<div class="container-fluid py-2">
|
||||
|
||||
{{-- Top info cards (container / date / status) --}}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Container Name</small>
|
||||
<div class="fw-semibold">{{ $container->container_name ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Container No</small>
|
||||
<div class="fw-semibold">{{ $container->container_number ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Container Date</small>
|
||||
<div class="fw-semibold">
|
||||
{{ $container->container_date ? \Carbon\Carbon::parse($container->container_date)->format('d-m-Y') : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Status</small>
|
||||
<div class="fw-semibold text-capitalize">{{ $container->status ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Totals (CTN / Qty / CBM / KG) --}}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Total CTN</small>
|
||||
<div class="fw-semibold">{{ $summary['total_ctn'] ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Total Qty</small>
|
||||
<div class="fw-semibold">{{ $summary['total_qty'] ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Total CBM</small>
|
||||
<div class="fw-semibold">{{ $summary['total_cbm'] ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Total KG</small>
|
||||
<div class="fw-semibold">{{ $summary['total_kg'] ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Excel rows – same headings as container_show --}}
|
||||
@php
|
||||
$allHeadings = [];
|
||||
foreach ($container->rows as $row) {
|
||||
if (is_array($row->data)) {
|
||||
$allHeadings = array_unique(array_merge($allHeadings, array_keys($row->data)));
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="table-responsive" style="max-height: 500px; border-radius: 8px;">
|
||||
<table class="table table-sm table-bordered align-middle">
|
||||
<thead class="table-warning">
|
||||
<tr>
|
||||
<th style="width: 40px;">#</th>
|
||||
@foreach($allHeadings as $heading)
|
||||
<th>{{ $heading }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($container->rows as $index => $row)
|
||||
<tr>
|
||||
<td>{{ $index + 1 }}</td>
|
||||
@foreach($allHeadings as $heading)
|
||||
@php
|
||||
$val = is_array($row->data) ? ($row->data[$heading] ?? '') : '';
|
||||
@endphp
|
||||
<td>{{ $val }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ count($allHeadings) + 1 }}" class="text-center text-muted py-3">
|
||||
No Excel rows for this container.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,621 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Dashboard')
|
||||
@section('page-title', 'Chat Support Dashboard')
|
||||
|
||||
@section('content')
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h4>Welcome to the Admin chat</h4>
|
||||
<p>Here you can manage all system modules.</p>
|
||||
</div>
|
||||
<style>
|
||||
:root {
|
||||
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--success-gradient: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
--danger-gradient: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%);
|
||||
--warning-gradient: linear-gradient(135deg, #f7971e 0%, #ffd200 100%);
|
||||
--info-gradient: linear-gradient(135deg, #56ccf2 0%, #2f80ed 100%);
|
||||
--card-shadow: 0 3px 10px rgba(0,0,0,0.05);
|
||||
--card-shadow-hover: 0 6px 16px rgba(0,0,0,0.10);
|
||||
--border-radius: 8px;
|
||||
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chat-dashboard {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 0.75rem;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: clamp(1.4rem, 2.5vw, 2rem);
|
||||
font-weight: 800;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin: 0 0 0.4rem 0;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dashboard-title::before {
|
||||
content: '💬';
|
||||
position: absolute;
|
||||
left: -1.6rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 1.3rem;
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% { transform: translateY(-50%) translateY(0); }
|
||||
40% { transform: translateY(-50%) translateY(-5px); }
|
||||
60% { transform: translateY(-50%) translateY(-2px); }
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
color: #64748b;
|
||||
font-size: 0.8rem;
|
||||
max-width: 380px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* 🔔 GLOBAL NEW MESSAGE COUNTER */
|
||||
.global-notify {
|
||||
margin: 0 auto 0.75rem auto;
|
||||
max-width: 320px;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(15,23,42,0.03);
|
||||
border: 1px dashed #cbd5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.78rem;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.global-notify-badge {
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
min-width: 18px;
|
||||
padding: 0 0.35rem;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 0 0 2px rgba(254, 226, 226, 0.8);
|
||||
}
|
||||
|
||||
.global-notify.d-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tickets-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.75rem 0.9rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
/* max-height / overflow काढले, जेणेकरून बाहेरचा page scroll वापरला जाईल */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tickets-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.6rem;
|
||||
padding-bottom: 0.45rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.tickets-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.tickets-count {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ticket-item {
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.6rem;
|
||||
margin-bottom: 0.3rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: var(--primary-gradient);
|
||||
transform: scaleY(0);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.ticket-item:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--card-shadow-hover);
|
||||
border-color: rgba(102, 126, 234, 0.35);
|
||||
}
|
||||
|
||||
.ticket-item:hover::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.ticket-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.ticket-avatar {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 6px;
|
||||
background: var(--info-gradient);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 3px 8px rgba(86, 204, 242, 0.25);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-avatar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
transition: left 0.4s;
|
||||
}
|
||||
|
||||
.ticket-item:hover .ticket-avatar::after {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.ticket-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ticket-name {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 0 0 0.08rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.unread-count {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 6px rgba(239, 68, 68, 0.4);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.ticket-preview {
|
||||
color: #64748b;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.25;
|
||||
margin-bottom: 0.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
max-height: 1.8em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-time {
|
||||
font-size: 0.65rem;
|
||||
color: #94a3b8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
.ticket-time svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ticket-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 0.3rem;
|
||||
border-top: 1px dashed #e5e7eb;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.14rem 0.45rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.status-open { background: var(--success-gradient); color: white; }
|
||||
.status-closed{ background: var(--danger-gradient); color: white; }
|
||||
|
||||
.chat-btn {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
font-size: 0.7rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.25);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.35);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat-btn::after {
|
||||
content: '→';
|
||||
transition: margin-left 0.25s ease;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.chat-btn:hover::after {
|
||||
margin-left: 0.18rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 1.6rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: var(--border-radius);
|
||||
border: 2px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 2.8rem;
|
||||
margin-bottom: 0.9rem;
|
||||
display: block;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.empty-subtitle {
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.ticket-id {
|
||||
background: rgba(102, 126, 234, 0.08);
|
||||
color: #667eea;
|
||||
padding: 0.12rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
.new-message-dot {
|
||||
position: absolute;
|
||||
top: 0.45rem;
|
||||
right: 0.5rem;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: #ef4444;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.3);
|
||||
animation: blink 1.5s infinite;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(1.1); }
|
||||
}
|
||||
|
||||
/* इथे आता inner scroll नाही */
|
||||
.tickets-list {
|
||||
/* flex: 1; काढला, overflow काढला, parent + body scroll वापरेल */
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar-thumb {
|
||||
background: var(--primary-gradient);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chat-dashboard {
|
||||
padding: 0.6rem;
|
||||
}
|
||||
|
||||
.tickets-container {
|
||||
/* max-height काढलेले, mobile वरही outer scroll */
|
||||
}
|
||||
|
||||
.ticket-header {
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.ticket-footer {
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.chat-btn {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="chat-dashboard">
|
||||
<!-- Header -->
|
||||
<div class="dashboard-header">
|
||||
<h1 class="dashboard-title">Live Chat Dashboard</h1>
|
||||
<p class="dashboard-subtitle">
|
||||
Monitor customer conversations with real-time updates
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 🔔 GLOBAL NEW MESSAGES NOTIFICATION -->
|
||||
<div id="globalNewMessageBox" class="global-notify d-none">
|
||||
<span>New messages:</span>
|
||||
<span id="globalNewMessageCount" class="global-notify-badge">0</span>
|
||||
</div>
|
||||
|
||||
<!-- Tickets Container -->
|
||||
<div class="tickets-container">
|
||||
<div class="tickets-header">
|
||||
<div>
|
||||
<h2 class="tickets-title">
|
||||
📋 Active Conversations
|
||||
<span class="tickets-count">{{ $tickets->count() }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($tickets->count() === 0)
|
||||
<!-- Empty State -->
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">💬</div>
|
||||
<h3 class="empty-title">No Active Conversations</h3>
|
||||
<p class="empty-subtitle">
|
||||
Customer conversations will appear here with real-time notifications.
|
||||
</p>
|
||||
<div class="ticket-id">Ready for support requests</div>
|
||||
</div>
|
||||
@else
|
||||
<!-- Tickets List -->
|
||||
<div class="tickets-list">
|
||||
@foreach($tickets as $ticket)
|
||||
<div class="ticket-item" data-ticket-id="{{ $ticket->id }}">
|
||||
@if($ticket->unread_count > 0)
|
||||
<div class="new-message-dot"></div>
|
||||
@endif
|
||||
|
||||
<div class="ticket-header">
|
||||
<div class="ticket-avatar">
|
||||
{{ strtoupper(substr($ticket->user->customer_name ?? $ticket->user->name, 0, 1)) }}
|
||||
</div>
|
||||
<div class="ticket-content">
|
||||
<div class="ticket-name">
|
||||
{{ $ticket->user->customer_name ?? $ticket->user->name }}
|
||||
@if($ticket->unread_count > 0)
|
||||
<span id="badge-{{ $ticket->id }}" class="unread-count">
|
||||
{{ $ticket->unread_count }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@php
|
||||
$lastMsg = $ticket->messages()->latest()->first();
|
||||
@endphp
|
||||
|
||||
<div class="ticket-preview">
|
||||
@if($lastMsg)
|
||||
@if($lastMsg->message)
|
||||
{{ Str::limit($lastMsg->message, 45) }}
|
||||
@elseif(Str::startsWith($lastMsg->file_type, 'image'))
|
||||
📷 Photo shared
|
||||
@else
|
||||
📎 File attached
|
||||
@endif
|
||||
@else
|
||||
<em>Conversation started</em>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($lastMsg)
|
||||
<div class="ticket-time">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
|
||||
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
|
||||
</svg>
|
||||
{{ $lastMsg->created_at->diffForHumans() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ticket-footer">
|
||||
<span class="status-badge status-{{ $ticket->status }}">
|
||||
{{ ucfirst($ticket->status) }}
|
||||
</span>
|
||||
<a href="{{ route('admin.chat.open', $ticket->id) }}" class="chat-btn">
|
||||
Open Chat
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
// -------------------------------
|
||||
// WAIT FOR ECHO READY (DEFINE IT)
|
||||
// -------------------------------
|
||||
function waitForEcho(callback, retries = 40) {
|
||||
if (window.Echo) {
|
||||
console.log('%c[ECHO] Ready (Admin List)', 'color: green; font-weight: bold;');
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (retries <= 0) {
|
||||
console.error('[ECHO] Failed to initialize');
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => waitForEcho(callback, retries - 1), 200);
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// LISTEN FOR REALTIME MESSAGES
|
||||
// -------------------------------
|
||||
waitForEcho(() => {
|
||||
console.log('[ADMIN LIST] Listening for new messages...');
|
||||
|
||||
const globalBox = document.getElementById('globalNewMessageBox');
|
||||
const globalCount = document.getElementById('globalNewMessageCount');
|
||||
let totalNewMessages = 0;
|
||||
|
||||
window.Echo.private('admin.chat')
|
||||
.listen('.NewChatMessage', (event) => {
|
||||
|
||||
// only USER → ADMIN messages
|
||||
if (event.sender_type !== 'App\\Models\\User') return;
|
||||
|
||||
const ticketId = event.ticket_id;
|
||||
|
||||
// 1) UPDATE PER-TICKET BADGE
|
||||
const badge = document.getElementById(`badge-${ticketId}`);
|
||||
if (badge) {
|
||||
let count = parseInt(badge.innerText || 0);
|
||||
badge.innerText = count + 1;
|
||||
badge.classList.remove('d-none');
|
||||
}
|
||||
|
||||
// 2) UPDATE GLOBAL NEW MESSAGE COUNTER
|
||||
totalNewMessages++;
|
||||
if (globalBox && globalCount) {
|
||||
globalBox.classList.remove('d-none');
|
||||
globalCount.innerText = totalNewMessages;
|
||||
}
|
||||
|
||||
// 3) त्या ticket ला यादीत सर्वात वर आणा आणि स्क्रोल करा
|
||||
const listContainer = document.querySelector('.tickets-list');
|
||||
const ticketEl = document.querySelector(`.ticket-item[data-ticket-id="${ticketId}"]`);
|
||||
|
||||
if (listContainer && ticketEl) {
|
||||
if (ticketEl.previousElementSibling) {
|
||||
listContainer.insertBefore(ticketEl, listContainer.firstElementChild);
|
||||
}
|
||||
|
||||
const topOffset = ticketEl.offsetTop;
|
||||
window.scrollTo({
|
||||
top: listContainer.offsetTop + topOffset - 20,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[ADMIN LIST] Badge/global counter updated for ticket', ticketId);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
1065
resources/views/admin/chat_window.blade.php
Normal file
1065
resources/views/admin/chat_window.blade.php
Normal file
File diff suppressed because it is too large
Load Diff
855
resources/views/admin/container.blade.php
Normal file
855
resources/views/admin/container.blade.php
Normal file
@@ -0,0 +1,855 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Containers')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #4c6fff;
|
||||
--primary-gradient: linear-gradient(135deg, #4c6fff, #8e54e9);
|
||||
--success-color: #10b981;
|
||||
--warning-color: #f59e0b;
|
||||
--danger-color: #ef4444;
|
||||
--info-color: #3b82f6;
|
||||
--light-bg: #f8fafc;
|
||||
--dark-text: #1e293b;
|
||||
--gray-text: #64748b;
|
||||
--border-color: #e2e8f0;
|
||||
--shadow-sm: 0 1px 3px rgba(0,0,0,0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1);
|
||||
--shadow-lg: 0 10px 25px -5px rgba(0,0,0,0.1);
|
||||
--radius-lg: 16px;
|
||||
--radius-md: 12px;
|
||||
--radius-sm: 8px;
|
||||
}
|
||||
|
||||
.containers-wrapper {
|
||||
min-height: calc(100vh - 180px);
|
||||
padding: 20px 15px;
|
||||
background: linear-gradient(135deg, #f6f9ff 0%, #f0f4ff 100%);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
color: var(--gray-text);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.add-container-btn {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 28px;
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 12px rgba(76, 111, 255, 0.3);
|
||||
}
|
||||
|
||||
.add-container-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(76, 111, 255, 0.4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.add-container-btn i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: 1px solid rgba(255,255,255,0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.filter-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--dark-text);
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-title i { color: var(--primary-color); }
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-group { position: relative; }
|
||||
|
||||
.filter-group label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-text);
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.filter-input, .filter-select, .filter-date {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 14px;
|
||||
color: var(--dark-text);
|
||||
background: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.filter-input:focus, .filter-select:focus, .filter-date:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(76, 111, 255, 0.1);
|
||||
}
|
||||
|
||||
.filter-input::placeholder { color: #94a3b8; }
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.apply-btn {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 46px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.apply-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 20px rgba(76, 111, 255, 0.3);
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: white;
|
||||
color: var(--gray-text);
|
||||
border: 2px solid var(--border-color);
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 46px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.main-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-lg);
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid rgba(255,255,255,0.9);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 24px;
|
||||
background: linear-gradient(135deg, #4c6fff, #8e54e9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.card-header h2 i { color: white; }
|
||||
|
||||
.stats-badge {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
margin-left: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.container-item {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.container-item:hover {
|
||||
background: #f8fafc;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.container-item:last-child { border-bottom: none; }
|
||||
|
||||
.container-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.container-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.container-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-gradient);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4px 12px rgba(76, 111, 255, 0.3);
|
||||
}
|
||||
|
||||
.container-details h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--dark-text);
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.container-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--gray-text);
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.meta-item i {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* STATUS DROPDOWN (badge look) */
|
||||
.status-dropdown {
|
||||
position: relative;
|
||||
min-width: 190px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status-dropdown-toggle {
|
||||
padding: 8px 16px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: #ffffff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--dark-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.status-dropdown-toggle span { white-space: nowrap; }
|
||||
|
||||
.status-dropdown-menu {
|
||||
position: absolute;
|
||||
top: -230%;
|
||||
right: 0;
|
||||
z-index: 30;
|
||||
background: #ffffff;
|
||||
border-radius: 14px;
|
||||
padding: 8px 0;
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: 1px solid var(--border-color);
|
||||
width: 220px;
|
||||
max-height: 340px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status-dropdown-menu.open { display: block; }
|
||||
|
||||
.status-option {
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
color: var(--dark-text);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: background 0.15s;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.status-option:hover { background: #eef2ff; }
|
||||
|
||||
.status-option .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
.status-option.active .dot {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
/* COLOR MAPPING per status – dropdown tint + main toggle text color */
|
||||
.status-option.status-container-ready { background: #eff6ff; color: #1d4ed8; }
|
||||
.status-option.status-export-custom { background: #fff7ed; color: #b45309; }
|
||||
.status-option.status-international-transit { background: #f5f3ff; color: #4c1d95; }
|
||||
.status-option.status-arrived-at-india { background: #ecfdf5; color: #15803d; }
|
||||
.status-option.status-import-custom { background: #fffbeb; color: #92400e; }
|
||||
.status-option.status-warehouse { background: #f4f4f5; color: #374151; }
|
||||
.status-option.status-domestic-distribution { background: #faf5ff; color: #6d28d9; }
|
||||
.status-option.status-out-for-delivery { background: #eff6ff; color: #1d4ed8; }
|
||||
.status-option.status-delivered { background: #ecfdf5; color: #15803d; }
|
||||
|
||||
.status-dropdown-toggle.status-container-ready { background: #eff6ff; color: #1d4ed8; }
|
||||
.status-dropdown-toggle.status-export-custom { background: #fff7ed; color: #b45309; }
|
||||
.status-dropdown-toggle.status-international-transit { background: #f5f3ff; color: #4c1d95; }
|
||||
.status-dropdown-toggle.status-arrived-at-india { background: #ecfdf5; color: #15803d; }
|
||||
.status-dropdown-toggle.status-import-custom { background: #fffbeb; color: #92400e; }
|
||||
.status-dropdown-toggle.status-warehouse { background: #f4f4f5; color: #374151; }
|
||||
.status-dropdown-toggle.status-domestic-distribution { background: #faf5ff; color: #6d28d9; }
|
||||
.status-dropdown-toggle.status-out-for-delivery { background: #eff6ff; color: #1d4ed8; }
|
||||
.status-dropdown-toggle.status-delivered { background: #ecfdf5; color: #15803d; }
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
background: #e0f2fe;
|
||||
color: #0369a1;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
background: #0ea5e9;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.update-form { position: relative; }
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.no-results-icon {
|
||||
font-size: 64px;
|
||||
color: var(--border-color);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.no-results h4 {
|
||||
font-size: 18px;
|
||||
color: var(--gray-text);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.no-results p {
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
box-shadow: var(--shadow-md);
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.success-message i { font-size: 20px; }
|
||||
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.totals-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
border-radius: var(--radius-md);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.total-card {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.total-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.total-value {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.total-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-text);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
.add-container-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.filter-grid { grid-template-columns: 1fr; }
|
||||
.container-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.update-form { width: 100%; }
|
||||
.status-dropdown { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="containers-wrapper">
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>Container Management</h1>
|
||||
<div class="header-subtitle">
|
||||
Manage all containers, track status, and view entries in real-time
|
||||
</div>
|
||||
</div>
|
||||
@can('container.create')
|
||||
<a href="{{ route('containers.create') }}" class="add-container-btn">
|
||||
<i class="fas fa-plus-circle"></i>
|
||||
Add New Container
|
||||
</a>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="success-message">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>{{ session('success') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="filter-card">
|
||||
<div class="filter-title">
|
||||
<i class="fas fa-filter"></i>
|
||||
Filter Containers
|
||||
</div>
|
||||
<form method="GET" class="filter-grid">
|
||||
<div class="filter-group">
|
||||
<label><i class="fas fa-search"></i> Search</label>
|
||||
<input type="text" name="search" class="filter-input"
|
||||
placeholder="Search by container name or number..."
|
||||
value="{{ request('search') }}">
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label><i class="fas fa-tag"></i> Status</label>
|
||||
<select name="status" class="filter-select">
|
||||
<option value="">All Status</option>
|
||||
<option value="container-ready" {{ request('status') == 'container-ready' ? 'selected' : '' }}>Container Ready</option>
|
||||
<option value="export-custom" {{ request('status') == 'export-custom' ? 'selected' : '' }}>Export Custom</option>
|
||||
<option value="international-transit" {{ request('status') == 'international-transit' ? 'selected' : '' }}>International Transit</option>
|
||||
<option value="arrived-at-india" {{ request('status') == 'arrived-at-india' ? 'selected' : '' }}>Arrived at India</option>
|
||||
<option value="import-custom" {{ request('status') == 'import-custom' ? 'selected' : '' }}>Import Custom</option>
|
||||
<option value="warehouse" {{ request('status') == 'warehouse' ? 'selected' : '' }}>Warehouse</option>
|
||||
<option value="domestic-distribution" {{ request('status') == 'domestic-distribution' ? 'selected' : '' }}>Domestic Distribution</option>
|
||||
<option value="out-for-delivery" {{ request('status') == 'out-for-delivery' ? 'selected' : '' }}>Out for Delivery</option>
|
||||
<option value="delivered" {{ request('status') == 'delivered' ? 'selected' : '' }}>Delivered</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label><i class="fas fa-calendar"></i> Date</label>
|
||||
<input type="date" name="date" class="filter-date" value="{{ request('date') }}">
|
||||
</div>
|
||||
|
||||
<div class="filter-actions">
|
||||
<button type="submit" class="apply-btn">
|
||||
<i class="fas fa-search"></i> Apply Filters
|
||||
</button>
|
||||
<a href="{{ route('containers.index') }}" class="reset-btn">
|
||||
<i class="fas fa-redo"></i> Reset
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="main-card">
|
||||
<div class="card-header">
|
||||
<h2>
|
||||
<i class="fas fa-boxes"></i>
|
||||
Containers List
|
||||
<span class="stats-badge">{{ $containers->count() }} containers</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@if($containers->isEmpty())
|
||||
<div class="no-results">
|
||||
<div class="no-results-icon">
|
||||
<i class="fas fa-box-open"></i>
|
||||
</div>
|
||||
<h4>No containers found</h4>
|
||||
<p>Get started by creating your first container</p>
|
||||
</div>
|
||||
@else
|
||||
@php
|
||||
$labels = [
|
||||
'container-ready' => 'Container Ready',
|
||||
'export-custom' => 'Export Custom',
|
||||
'international-transit' => 'International Transit',
|
||||
'arrived-at-india' => 'Arrived at India',
|
||||
'import-custom' => 'Import Custom',
|
||||
'warehouse' => 'Warehouse',
|
||||
'domestic-distribution' => 'Domestic Distribution',
|
||||
'out-for-delivery' => 'Out for Delivery',
|
||||
'delivered' => 'Delivered',
|
||||
];
|
||||
@endphp
|
||||
|
||||
@foreach($containers as $container)
|
||||
@php
|
||||
$status = $container->status ?? 'container-ready';
|
||||
$statusLabel = $labels[$status] ?? ucfirst(str_replace('-', ' ', $status));
|
||||
@endphp
|
||||
|
||||
<div class="container-item">
|
||||
<div class="container-header">
|
||||
<div class="container-info">
|
||||
<div class="container-avatar">
|
||||
{{ strtoupper(substr($container->container_name, 0, 2)) }}
|
||||
</div>
|
||||
<div class="container-details">
|
||||
<h3>{{ $container->container_name }}</h3>
|
||||
<div class="container-meta">
|
||||
<div class="meta-item">
|
||||
<i class="fas fa-hashtag"></i>
|
||||
<span>{{ $container->container_number }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<i class="fas fa-calendar"></i>
|
||||
<span>{{ $container->container_date?->format('M d, Y') ?: 'No date' }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<i class="fas fa-list"></i>
|
||||
<span>{{ $container->rows->count() }} entries</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
@can('containers.update_status')
|
||||
<form action="{{ route('containers.update-status', $container->id) }}"
|
||||
method="POST"
|
||||
class="update-form ajax-status-form"
|
||||
data-container-id="{{ $container->id }}">
|
||||
@csrf
|
||||
|
||||
@php $statusClass = 'status-' . $status; @endphp
|
||||
|
||||
<div class="status-dropdown">
|
||||
<div class="status-dropdown-toggle {{ $statusClass }}">
|
||||
<span class="status-dropdown-label">
|
||||
{{ $statusLabel }}
|
||||
</span>
|
||||
<i class="fas fa-chevron-down" style="font-size:11px;color:#4b5563;"></i>
|
||||
</div>
|
||||
<div class="status-dropdown-menu">
|
||||
@foreach($labels as $value => $label)
|
||||
@php $optClass = 'status-' . $value; @endphp
|
||||
<div class="status-option {{ $optClass }} {{ $status === $value ? 'active' : '' }}"
|
||||
data-status="{{ $value }}">
|
||||
<span class="dot"></span>
|
||||
<span>{{ $label }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endcan
|
||||
|
||||
@can('container.update')
|
||||
<a href="{{ route('containers.show', $container->id) }}" class="action-btn view-btn">
|
||||
<i class="fas fa-eye"></i> View
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
@can('container.delete')
|
||||
<form action="{{ route('containers.destroy', $container->id) }}" method="POST"
|
||||
class="delete-form"
|
||||
data-container-id="{{ $container->id }}">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="action-btn delete-btn">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</form>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="totals-section">
|
||||
<div class="total-card">
|
||||
<div class="total-value">{{ number_format($container->summary['total_ctn'], 1) }}</div>
|
||||
<div class="total-label">Total CTN</div>
|
||||
</div>
|
||||
<div class="total-card">
|
||||
<div class="total-value">{{ number_format($container->summary['total_qty'], 0) }}</div>
|
||||
<div class="total-label">Total QTY</div>
|
||||
</div>
|
||||
<div class="total-card">
|
||||
<div class="total-value">{{ number_format($container->summary['total_cbm'], 3) }}</div>
|
||||
<div class="total-label">Total CBM</div>
|
||||
</div>
|
||||
<div class="total-card">
|
||||
<div class="total-value">{{ number_format($container->summary['total_kg'], 1) }}</div>
|
||||
<div class="total-label">Total KG</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// STATUS DROPDOWN
|
||||
document.querySelectorAll('.status-dropdown').forEach(function (wrapper) {
|
||||
const toggle = wrapper.querySelector('.status-dropdown-toggle');
|
||||
const menu = wrapper.querySelector('.status-dropdown-menu');
|
||||
|
||||
toggle.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
document.querySelectorAll('.status-dropdown-menu.open').forEach(m => {
|
||||
if (m !== menu) m.classList.remove('open');
|
||||
});
|
||||
menu.classList.toggle('open');
|
||||
});
|
||||
|
||||
menu.querySelectorAll('.status-option').forEach(function (opt) {
|
||||
opt.addEventListener('click', function () {
|
||||
const status = this.dataset.status;
|
||||
const form = wrapper.closest('form');
|
||||
const labelEl = wrapper.querySelector('.status-dropdown-label');
|
||||
const toggleEl= wrapper.querySelector('.status-dropdown-toggle');
|
||||
|
||||
// UI: dropdown label + active item
|
||||
menu.querySelectorAll('.status-option').forEach(o => o.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
labelEl.textContent = this.querySelector('span:nth-child(2)').textContent;
|
||||
menu.classList.remove('open');
|
||||
|
||||
// toggle रंग class reset करून नवा status-* दे
|
||||
toggleEl.className = 'status-dropdown-toggle';
|
||||
toggleEl.classList.add('status-' + status);
|
||||
|
||||
const url = form.getAttribute('action');
|
||||
const token = form.querySelector('input[name="_token"]').value;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('_token', token);
|
||||
formData.append('status', status);
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(async res => {
|
||||
let data = null;
|
||||
try { data = await res.json(); } catch (e) {}
|
||||
if (!res.ok || !data || !data.success) {
|
||||
alert('Status update failed');
|
||||
return;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
alert('Network error while updating status');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', function () {
|
||||
document.querySelectorAll('.status-dropdown-menu.open')
|
||||
.forEach(m => m.classList.remove('open'));
|
||||
});
|
||||
|
||||
// DELETE VIA AJAX
|
||||
document.querySelectorAll('.delete-form').forEach(function (form) {
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!confirm('Are you sure you want to delete this container and all its entries?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = form.getAttribute('action');
|
||||
const token = form.querySelector('input[name="_token"]').value;
|
||||
const item = form.closest('.container-item');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('_token', token);
|
||||
formData.append('_method', 'DELETE');
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(async res => {
|
||||
let data = null;
|
||||
try { data = await res.json(); } catch (e) {}
|
||||
|
||||
if (!res.ok || !data || !data.success) {
|
||||
alert('Delete failed');
|
||||
return;
|
||||
}
|
||||
|
||||
if (item) {
|
||||
item.style.opacity = '0';
|
||||
item.style.transform = 'translateX(-10px)';
|
||||
setTimeout(() => item.remove(), 200);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
alert('Network error while deleting container');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
@endsection
|
||||
349
resources/views/admin/container_create.blade.php
Normal file
349
resources/views/admin/container_create.blade.php
Normal file
@@ -0,0 +1,349 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Add Container')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
.cm-add-wrapper { padding: 10px 0 20px 0; }
|
||||
.cm-add-header-card {
|
||||
border-radius: 14px;
|
||||
border: none;
|
||||
margin-bottom: 18px;
|
||||
background: linear-gradient(90deg,#4c6fff,#8e54e9);
|
||||
box-shadow: 0 6px 18px rgba(15,35,52,0.18);
|
||||
color: #ffffff;
|
||||
}
|
||||
.cm-add-header-card .card-body {
|
||||
padding: 14px 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
.cm-add-title { margin: 0; font-size: 20px; font-weight: 600; }
|
||||
.cm-add-sub { font-size: 12px; opacity: 0.9; }
|
||||
|
||||
.cm-add-main-card {
|
||||
border-radius: 14px;
|
||||
border: none;
|
||||
box-shadow: 0 6px 18px rgba(15,35,52,0.12);
|
||||
}
|
||||
.cm-add-main-card .card-header {
|
||||
background:#ffffff;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
padding: 10px 18px;
|
||||
}
|
||||
.cm-add-main-card .card-header h5 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cm-form-label { font-size: 13px; font-weight: 500; color:#495057; margin-bottom: 4px; }
|
||||
.cm-form-control {
|
||||
font-size: 13px;
|
||||
border-radius: 10px;
|
||||
border:1px solid #d0d7e2;
|
||||
padding: 8px 11px;
|
||||
}
|
||||
.cm-form-control:focus {
|
||||
border-color:#4c6fff;
|
||||
box-shadow:0 0 0 0.15rem rgba(76,111,255,.25);
|
||||
}
|
||||
|
||||
.cm-help-text { font-size: 11px; color:#868e96; margin-top: 2px; }
|
||||
.cm-btn-primary { border-radius: 20px; padding: 6px 22px; font-size: 13px; font-weight: 500; }
|
||||
.cm-btn-secondary { border-radius: 20px; padding: 6px 18px; font-size: 13px; }
|
||||
|
||||
.error-card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #f5c2c7;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.error-row-box {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.error-item {
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.error-item span {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container-fluid cm-add-wrapper">
|
||||
|
||||
<div class="card cm-add-header-card">
|
||||
<div class="card-body">
|
||||
<div>
|
||||
<h4 class="cm-add-title">Create New Container</h4>
|
||||
<div class="cm-add-sub">
|
||||
Add container details and upload Kent loading list Excel file.
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('containers.index') }}" class="btn btn-light btn-sm">
|
||||
Back to Containers
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card cm-add-main-card">
|
||||
<div class="card-header">
|
||||
<h5>Add Container</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
{{-- SUCCESS --}}
|
||||
@if (session('success'))
|
||||
<div class="alert alert-success">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- VALIDATION --}}
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<ul class="mb-0">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- COMBINED ERROR PANEL (summary text) --}}
|
||||
@if(session('formula_errors') || session('mark_errors'))
|
||||
|
||||
<div class="card error-card mb-3">
|
||||
<div class="card-header bg-danger text-white small">
|
||||
⚠ Excel Validation Issues Found
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="small mb-1">
|
||||
Some rows in your Excel file have formula or mark issues.
|
||||
See the table below, and detailed messages after the table.
|
||||
</div>
|
||||
<ul class="small mb-0 ps-3">
|
||||
<li>Red highlighted rows indicate formula mismatches in the uploaded Excel data.</li>
|
||||
<li>Yellow highlighted rows indicate marks from the Excel file that do not match any record in the system.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 1) Excel-style table at top --}}
|
||||
@php
|
||||
$formulaErrors = session('formula_errors') ?? [];
|
||||
$markErrors = session('mark_errors') ?? [];
|
||||
|
||||
$allRowsData = [];
|
||||
|
||||
foreach ($formulaErrors as $fe) {
|
||||
if (!empty($fe['data'] ?? null)) {
|
||||
$allRowsData[] = $fe['data'];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($markErrors as $me) {
|
||||
if (!empty($me['data'] ?? null)) {
|
||||
$allRowsData[] = $me['data'];
|
||||
}
|
||||
}
|
||||
|
||||
$headings = [];
|
||||
if (!empty($allRowsData)) {
|
||||
$headings = array_keys($allRowsData[0]);
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if(!empty($headings))
|
||||
<div class="card mb-3">
|
||||
<div class="card-header small">
|
||||
Error rows in Excel view
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
<div class="table-responsive" style="max-height:260px; overflow:auto;">
|
||||
<table class="table table-sm table-bordered mb-0" style="font-size:11.5px;">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Excel Row</th>
|
||||
<th>Mark No</th>
|
||||
@foreach($headings as $head)
|
||||
<th>{{ $head }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{-- Formula error rows (red – formula mismatch, critical) --}}
|
||||
@foreach($formulaErrors as $fe)
|
||||
@php $rowData = $fe['data'] ?? []; @endphp
|
||||
@if(!empty($rowData))
|
||||
<tr class="table-danger">
|
||||
<td>{{ $fe['excel_row'] }}</td>
|
||||
<td>{{ $fe['mark_no'] }}</td>
|
||||
@foreach($headings as $head)
|
||||
<td>{{ $rowData[$head] ?? '' }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
{{-- Mark error rows (yellow – mark not found, warning) --}}
|
||||
@foreach($markErrors as $me)
|
||||
@php $rowData = $me['data'] ?? []; @endphp
|
||||
@if(!empty($rowData))
|
||||
<tr class="table-warning">
|
||||
<td>{{ $me['excel_row'] }}</td>
|
||||
<td>{{ $me['mark_no'] }}</td>
|
||||
@foreach($headings as $head)
|
||||
<td>{{ $rowData[$head] ?? '' }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endif
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 2) Detailed per-row error boxes BELOW the table --}}
|
||||
<div class="card error-card mb-3">
|
||||
<div class="card-header bg-light small">
|
||||
Detailed error messages
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
{{-- Formula Errors (detailed) --}}
|
||||
@if(session('formula_errors'))
|
||||
@foreach(session('formula_errors') as $fe)
|
||||
<div class="error-row-box">
|
||||
<div class="error-title">
|
||||
Row {{ $fe['excel_row'] }}
|
||||
@if($fe['mark_no']) | Mark: {{ $fe['mark_no'] }} @endif
|
||||
@if($fe['description']) | {{ $fe['description'] }} @endif
|
||||
</div>
|
||||
|
||||
@foreach($fe['errors'] as $field => $detail)
|
||||
<div class="error-item text-danger">
|
||||
❌ <span>{{ $field }}</span> →
|
||||
Expected: {{ number_format($detail['expected'],4) }}
|
||||
| Got: {{ number_format($detail['actual'],4) }}
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
{{-- Mark Errors (detailed) --}}
|
||||
@if(session('mark_errors'))
|
||||
@foreach(session('mark_errors') as $me)
|
||||
<div class="error-row-box bg-warning bg-opacity-10 border-warning">
|
||||
<div class="error-title">
|
||||
Row {{ $me['excel_row'] }}
|
||||
</div>
|
||||
<div class="error-item text-warning">
|
||||
❌ Mark <strong>{{ $me['mark_no'] }}</strong> not found in database
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endif
|
||||
|
||||
{{-- FORM --}}
|
||||
@if (!session('formula_errors') && !session('mark_errors'))
|
||||
<form action="{{ route('containers.store') }}" method="POST" enctype="multipart/form-data" class="mt-3">
|
||||
@csrf
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="cm-form-label">Container Name</label>
|
||||
<input type="text" name="container_name"
|
||||
class="form-control cm-form-control"
|
||||
value="{{ old('container_name') }}"
|
||||
placeholder="Enter container name">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="cm-form-label">Container Number</label>
|
||||
<input type="text" name="container_number"
|
||||
class="form-control cm-form-control"
|
||||
value="{{ old('container_number') }}"
|
||||
placeholder="Enter container number">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="cm-form-label">Container Date</label>
|
||||
<input type="date"
|
||||
name="container_date" {{-- name fix --}}
|
||||
id="containerdate" {{-- JS साठी जुना id ठेवला --}}
|
||||
class="form-control cm-form-control"
|
||||
value="{{ old('container_date') }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="cm-form-label">Loading List Excel</label>
|
||||
<input type="file" name="excel_file"
|
||||
class="form-control cm-form-control"
|
||||
accept=".xls,.xlsx">
|
||||
<div class="cm-help-text">
|
||||
Upload Kent loading list Excel file (.xls / .xlsx).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary cm-btn-primary">
|
||||
Save Container
|
||||
</button>
|
||||
<a href="{{ route('containers.index') }}" class="btn btn-outline-secondary cm-btn-secondary">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const dateInput = document.getElementById('containerdate');
|
||||
if (!dateInput) return;
|
||||
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
const todayStr = `${year}-${month}-${day}`;
|
||||
|
||||
// Todays date
|
||||
dateInput.min = todayStr;
|
||||
|
||||
// old date remove
|
||||
if (dateInput.value && dateInput.value < todayStr) {
|
||||
dateInput.value = todayStr;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
202
resources/views/admin/container_pdf.blade.php
Normal file
202
resources/views/admin/container_pdf.blade.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Container {{ $container->container_number }} Summary</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: DejaVu Sans, sans-serif;
|
||||
font-size: 10px;
|
||||
margin: 10px;
|
||||
background: #e5e7ff;
|
||||
}
|
||||
|
||||
/* COMMON CARD GRID – 4 equal columns, 2 rows */
|
||||
.card-grid {
|
||||
display: table;
|
||||
width: 100%;
|
||||
border-spacing: 8px 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.card-row {
|
||||
display: table-row;
|
||||
}
|
||||
.card {
|
||||
display: table-cell;
|
||||
width: 25%;
|
||||
padding: 7px 10px;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 12px rgba(15,35,52,0.18);
|
||||
color: #0f172a;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* INFO CARDS (FIRST ROW) */
|
||||
.info-id { background: #e0f2ff; border-left: 4px solid #2563eb; }
|
||||
.info-no { background: #dcfce7; border-left: 4px solid #22c55e; }
|
||||
.info-date { background: #fee2e2; border-left: 4px solid #ef4444; }
|
||||
.info-name { background: #fef9c3; border-left: 4px solid #f59e0b; }
|
||||
|
||||
/* TOTAL CARDS (SECOND ROW) */
|
||||
.total-ctn { background: #dbeafe; border-left: 4px solid #1d4ed8; }
|
||||
.total-qty { background: #bbf7d0; border-left: 4px solid #16a34a; }
|
||||
.total-cbm { background: #fef3c7; border-left: 4px solid #d97706; }
|
||||
.total-kg { background: #fecaca; border-left: 4px solid #dc2626; }
|
||||
|
||||
.label-text {
|
||||
font-size: 9px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
color: #4b5563;
|
||||
}
|
||||
.value-text-small {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
margin-top: 2px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.value-text-big {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* TABLE – solid yellow header like screenshot */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 6px;
|
||||
table-layout: fixed;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 4px 3px;
|
||||
text-align: center;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
th {
|
||||
background: #fbd85d; /* इथे solid yellow */
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
color: #0c0909;
|
||||
}
|
||||
td {
|
||||
font-size: 9px;
|
||||
color: #111827;
|
||||
}
|
||||
tr:nth-child(even) td {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
thead { display: table-header-group; }
|
||||
tr { page-break-inside: avoid; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@php
|
||||
$totalCtn = 0;
|
||||
$totalQty = 0;
|
||||
$totalCbm = 0.0;
|
||||
$totalKg = 0.0;
|
||||
|
||||
foreach ($container->rows as $row) {
|
||||
if (!is_array($row->data)) continue;
|
||||
foreach ($row->data as $h => $v) {
|
||||
$norm = strtoupper(str_replace([' ', '/', '-', '.'],'', $h));
|
||||
$val = is_numeric(str_replace([','], '', $v)) ? floatval(str_replace([','], '', $v)) : 0;
|
||||
|
||||
if (str_contains($norm, 'TOTALCTN') || $norm === 'CTN' || str_contains($norm,'TOTALCNTR') || str_contains($norm,'TOTALCARTON')) {
|
||||
$totalCtn += $val;
|
||||
}
|
||||
if (str_contains($norm,'TOTALQTY') || str_contains($norm,'ITLQTY') || str_contains($norm,'TTLQTY')) {
|
||||
$totalQty += $val;
|
||||
}
|
||||
if (str_contains($norm,'TOTALCBM') || str_contains($norm,'TTLCBM') || str_contains($norm,'ITLCBM')) {
|
||||
$totalCbm += $val;
|
||||
}
|
||||
if (str_contains($norm,'TOTALKG') || str_contains($norm,'TTKG')) {
|
||||
$totalKg += $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$allHeadings = [];
|
||||
foreach ($container->rows as $row) {
|
||||
if (is_array($row->data)) {
|
||||
$allHeadings = array_unique(array_merge($allHeadings, array_keys($row->data)));
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
{{-- TWO ROW GRID – FIRST: INFO / SECOND: TOTALS --}}
|
||||
<div class="card-grid">
|
||||
<div class="card-row">
|
||||
<div class="card info-id">
|
||||
<div class="label-text">Container ID</div>
|
||||
<div class="value-text-small">{{ $container->id }}</div>
|
||||
</div>
|
||||
<div class="card info-no">
|
||||
<div class="label-text">Container Number</div>
|
||||
<div class="value-text-small">{{ $container->container_number }}</div>
|
||||
</div>
|
||||
<div class="card info-date">
|
||||
<div class="label-text">Container Date</div>
|
||||
<div class="value-text-small">
|
||||
{{ $container->container_date ? $container->container_date->format('d-m-Y') : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card info-name">
|
||||
<div class="label-text">Container Name</div>
|
||||
<div class="value-text-small">
|
||||
{{ $container->container_name ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-row">
|
||||
<div class="card total-ctn">
|
||||
<div class="label-text">Total CTN</div>
|
||||
<div class="value-text-big">{{ number_format($totalCtn, 0) }}</div>
|
||||
</div>
|
||||
<div class="card total-qty">
|
||||
<div class="label-text">Total QTY</div>
|
||||
<div class="value-text-big">{{ number_format($totalQty, 0) }}</div>
|
||||
</div>
|
||||
<div class="card total-cbm">
|
||||
<div class="label-text">Total CBM</div>
|
||||
<div class="value-text-big">{{ number_format($totalCbm, 3) }}</div>
|
||||
</div>
|
||||
<div class="card total-kg">
|
||||
<div class="label-text">Total KG</div>
|
||||
<div class="value-text-big">{{ number_format($totalKg, 2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- FULL TABLE --}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:18px;">#</th>
|
||||
@foreach($allHeadings as $heading)
|
||||
<th>{{ $heading }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($container->rows as $index => $row)
|
||||
<tr>
|
||||
<td>{{ $index + 1 }}</td>
|
||||
@foreach($allHeadings as $heading)
|
||||
<td>{{ $row->data[$heading] ?? '' }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
891
resources/views/admin/container_show.blade.php
Normal file
891
resources/views/admin/container_show.blade.php
Normal file
@@ -0,0 +1,891 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Container Details')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap');
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.cm-header-card {
|
||||
background: linear-gradient(100deg, #4c6fff 0%, #8e54e9 100%);
|
||||
border-radius: 14px;
|
||||
border: none;
|
||||
box-shadow: 0 6px 24px rgba(76,111,255,0.22);
|
||||
margin-bottom: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
.cm-header-card .card-body {
|
||||
padding: 14px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.cm-header-title {
|
||||
margin: 0;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
.cm-header-sub {
|
||||
font-size: 12px;
|
||||
opacity: 0.88;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.cm-main-card {
|
||||
border-radius: 14px;
|
||||
border: none;
|
||||
box-shadow: 0 4px 20px rgba(15,35,52,0.10);
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
}
|
||||
.cm-main-card .card-header {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.cm-main-card .card-header h5 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1a2340;
|
||||
}
|
||||
|
||||
.cm-info-cards-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr)); /* 3 cards one row */
|
||||
gap: 14px;
|
||||
padding: 14px 18px 8px 18px;
|
||||
}
|
||||
.cm-info-card {
|
||||
border-radius: 16px;
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
box-shadow: 0 4px 16px rgba(15,35,52,0.12);
|
||||
border: 1px solid rgba(148,163,184,0.25);
|
||||
background: #fff;
|
||||
min-height: 70px;
|
||||
}
|
||||
.cm-info-card-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #0f172a;
|
||||
font-size: 16px;
|
||||
box-shadow: 0 3px 10px rgba(15,23,42,0.10);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cm-info-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
.cm-info-card-label {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cm-info-card-value {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cm-card-container {
|
||||
background: linear-gradient(135deg, #e0f2ff, #eef4ff);
|
||||
}
|
||||
.cm-card-container .cm-info-card-icon {
|
||||
background: linear-gradient(135deg, #2563eb, #4f46e5);
|
||||
color: #e5edff;
|
||||
}
|
||||
.cm-card-date {
|
||||
background: linear-gradient(135deg, #ecfdf3, #e0fbea);
|
||||
}
|
||||
.cm-card-date .cm-info-card-icon {
|
||||
background: linear-gradient(135deg, #16a34a, #22c55e);
|
||||
color: #ecfdf3;
|
||||
}
|
||||
.cm-card-excel {
|
||||
background: linear-gradient(135deg, #fff7ed, #fffbeb);
|
||||
}
|
||||
.cm-card-excel .cm-info-card-icon {
|
||||
background: linear-gradient(135deg, #f97316, #fb923c);
|
||||
color: #fff7ed;
|
||||
}
|
||||
|
||||
/* TOTAL BOXES */
|
||||
.cm-total-cards-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
padding: 4px 18px 14px 18px;
|
||||
}
|
||||
.cm-total-card {
|
||||
border-radius: 18px;
|
||||
padding: 12px 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
box-shadow: 0 8px 30px rgba(15,23,42,0.08);
|
||||
border: 1px solid rgba(148,163,184,0.25);
|
||||
}
|
||||
.cm-total-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 17px;
|
||||
flex-shrink: 0;
|
||||
color: #0f172a;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 14px rgba(15,23,42,0.15);
|
||||
}
|
||||
.cm-total-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
.cm-total-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #4b5563;
|
||||
}
|
||||
.cm-total-value {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: #111827;
|
||||
}
|
||||
.cm-total-card-ctn {
|
||||
background: linear-gradient(135deg, #e0f2ff, #eef2ff);
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
.cm-total-card-qty {
|
||||
background: linear-gradient(135deg, #dcfce7, #ecfdf5);
|
||||
border-left: 4px solid #22c55e;
|
||||
}
|
||||
.cm-total-card-cbm {
|
||||
background: linear-gradient(135deg, #fef9c3, #fffbeb);
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
.cm-total-card-kg {
|
||||
background: linear-gradient(135deg, #fee2e2, #fef2f2);
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
|
||||
.cm-filter-bar {
|
||||
padding: 4px 20px 0 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.cm-row-count {
|
||||
font-size: 12px;
|
||||
color: #8a93a6;
|
||||
font-weight: 500;
|
||||
}
|
||||
.cm-filter-input {
|
||||
max-width: 240px;
|
||||
font-size: 12px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #dde2ee;
|
||||
padding: 6px 14px;
|
||||
outline: none;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
.cm-filter-input:focus {
|
||||
border-color: #4c6fff;
|
||||
box-shadow: 0 0 0 3px rgba(76,111,255,0.1);
|
||||
}
|
||||
|
||||
.cm-table-scroll-outer {
|
||||
margin: 10px 14px 0 14px;
|
||||
border-radius: 14px;
|
||||
border: 1.5px solid #c9a359;
|
||||
box-shadow: 0 4px 14px rgba(76,111,255,0.18);
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.cm-table-wrapper {
|
||||
position: relative;
|
||||
min-width: 1200px;
|
||||
}
|
||||
|
||||
.cm-table {
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
}
|
||||
.cm-table thead tr th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
background: linear-gradient(100deg, #fde4b3 0%, #fbd48a 100%);
|
||||
color: #0c0909;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
padding: 11px 14px;
|
||||
border-bottom: 2px solid rgba(255,255,255,0.15);
|
||||
border-right: 1px solid rgba(255,255,255,0.18);
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
letter-spacing: 0.2px;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.35);
|
||||
}
|
||||
.cm-table thead tr th:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
.cm-table thead tr th:last-child {
|
||||
border-top-right-radius: 10px;
|
||||
border-right: none;
|
||||
}
|
||||
.cm-table thead tr th:first-child,
|
||||
.cm-table tbody tr td:first-child {
|
||||
width: 46px;
|
||||
min-width: 46px;
|
||||
max-width: 46px;
|
||||
text-align: center;
|
||||
}
|
||||
.cm-table tbody tr td {
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid #f0f3fb;
|
||||
border-right: 1px solid #f0f3fb;
|
||||
color: #2d3a55;
|
||||
font-size: 12.5px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
background: #fff;
|
||||
transition: background 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cm-table tbody tr td:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
.cm-table tbody tr:nth-child(even) td {
|
||||
background: #f8f9ff;
|
||||
}
|
||||
.cm-table tbody tr:hover td {
|
||||
background: #edf3ff !important;
|
||||
}
|
||||
.cm-row-num {
|
||||
color: #8a93a6;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cm-cell-input {
|
||||
width: 100%;
|
||||
min-width: 90px;
|
||||
background: transparent;
|
||||
border: 1.5px solid transparent;
|
||||
border-radius: 5px;
|
||||
font-size: 12.5px;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
color: #2d3a55;
|
||||
padding: 3px 6px;
|
||||
text-align: center;
|
||||
transition: border 0.15s, background 0.15s, box-shadow 0.15s;
|
||||
outline: none;
|
||||
}
|
||||
.cm-cell-input:hover {
|
||||
border-color: #c9d4f5;
|
||||
background: #f5f8ff;
|
||||
}
|
||||
.cm-cell-input:focus {
|
||||
border-color: #4c6fff;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 3px rgba(76,111,255,0.12);
|
||||
}
|
||||
.cm-cell-readonly {
|
||||
background: #f3f4ff;
|
||||
cursor: not-allowed;
|
||||
border-color: #e2e3ff;
|
||||
}
|
||||
|
||||
.cm-save-btn-floating {
|
||||
position: fixed;
|
||||
right: 26px;
|
||||
bottom: 22px;
|
||||
z-index: 50;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border-radius: 22px;
|
||||
padding: 8px 20px;
|
||||
background: linear-gradient(90deg, #4c6fff, #8e54e9);
|
||||
border: none;
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 16px rgba(76,111,255,0.35);
|
||||
transition: opacity 0.2s, transform 0.15s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.cm-save-btn-floating:hover {
|
||||
opacity: 0.94;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.cm-save-btn-floating.cm-disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cm-toast {
|
||||
position: fixed;
|
||||
right: 26px;
|
||||
bottom: 70px;
|
||||
background: #0f172a;
|
||||
color: #f9fafb;
|
||||
font-size: 12.5px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 8px 24px rgba(15,23,42,0.25);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s, transform 0.2s;
|
||||
transform: translateY(8px);
|
||||
z-index: 60;
|
||||
}
|
||||
.cm-toast.cm-show {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.cm-empty {
|
||||
padding: 30px 20px;
|
||||
color: #8a93a6;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cm-table-scroll-outer::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
.cm-table-scroll-outer::-webkit-scrollbar-track {
|
||||
background: #f0f3fb;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.cm-table-scroll-outer::-webkit-scrollbar-thumb {
|
||||
background: #c5cce8;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.cm-table-scroll-outer::-webkit-scrollbar-thumb:hover {
|
||||
background: #8a99d0;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.cm-total-cards-row {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.cm-header-card .card-body {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.cm-info-cards-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.cm-table-scroll-outer {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.cm-table-wrapper {
|
||||
min-width: 900px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 575px) {
|
||||
.cm-total-cards-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container-fluid cm-wrapper">
|
||||
<div class="card cm-header-card">
|
||||
<div class="card-body">
|
||||
<div>
|
||||
<h4 class="cm-header-title">Container {{ $container->container_number }}</h4>
|
||||
<div class="cm-header-sub">
|
||||
Edit loading list directly – like Excel. TT columns auto‑calculate from CTN, QTY, CBM, KG, PRICE.
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ route('containers.index') }}" class="btn btn-light btn-sm">Back to list</a>
|
||||
|
||||
<a href="{{ route('containers.download.pdf', $container->id) }}"
|
||||
class="btn btn-sm btn-outline-primary">
|
||||
Download PDF
|
||||
</a>
|
||||
|
||||
<a href="{{ route('containers.download.excel', $container->id) }}"
|
||||
class="btn btn-sm btn-outline-success">
|
||||
Download Excel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card cm-main-card">
|
||||
<div class="card-header">
|
||||
<h5>Container Information</h5>
|
||||
</div>
|
||||
|
||||
{{-- 3 INFO CARDS IN SINGLE ROW --}}
|
||||
<div class="cm-info-cards-row">
|
||||
<div class="cm-info-card cm-card-container">
|
||||
<div class="cm-info-card-icon">
|
||||
<i class="bi bi-box-seam"></i>
|
||||
</div>
|
||||
<div class="cm-info-card-body">
|
||||
<div class="cm-info-card-label">Container</div>
|
||||
<div class="cm-info-card-value">
|
||||
{{ $container->container_name ?? $container->container_number }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cm-info-card cm-card-date">
|
||||
<div class="cm-info-card-icon">
|
||||
<i class="bi bi-calendar-event"></i>
|
||||
</div>
|
||||
<div class="cm-info-card-body">
|
||||
<div class="cm-info-card-label">Date</div>
|
||||
<div class="cm-info-card-value">
|
||||
{{ $container->container_date ? $container->container_date->format('d-m-Y') : '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cm-info-card cm-card-excel">
|
||||
<div class="cm-info-card-icon">
|
||||
<i class="bi bi-file-earmark-excel"></i>
|
||||
</div>
|
||||
<div class="cm-info-card-body">
|
||||
<div class="cm-info-card-label">Excel File</div>
|
||||
<div class="cm-info-card-value">
|
||||
@if($container->excel_file)
|
||||
<a href="{{ url($container->excel_file) }}" target="_blank" style="color:#0f172a;text-decoration:none;">
|
||||
Download / View
|
||||
</a>
|
||||
@else
|
||||
<span style="color:#b0b8cc;">Not uploaded</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$totalCtn = 0;
|
||||
$totalQty = 0;
|
||||
$totalCbm = 0.0;
|
||||
$totalKg = 0.0;
|
||||
|
||||
if(!$container->rows->isEmpty()){
|
||||
foreach ($container->rows as $row) {
|
||||
if (!is_array($row->data)) continue;
|
||||
foreach ($row->data as $h => $v) {
|
||||
$norm = strtoupper(str_replace([' ', '/', '-', '.'],'', $h));
|
||||
$val = is_numeric(str_replace([','], '', $v)) ? floatval(str_replace([','], '', $v)) : 0;
|
||||
|
||||
if (str_contains($norm, 'TOTALCTN') || $norm === 'CTN' || str_contains($norm,'TOTALCNTR') || str_contains($norm,'TOTALCARTON')) {
|
||||
$totalCtn += $val;
|
||||
}
|
||||
|
||||
if (
|
||||
str_contains($norm,'TOTALQTY') ||
|
||||
str_contains($norm,'ITLQTY') ||
|
||||
str_contains($norm,'TTLQTY')
|
||||
) {
|
||||
$totalQty += $val;
|
||||
}
|
||||
|
||||
if (
|
||||
str_contains($norm,'TOTALCBM') ||
|
||||
str_contains($norm,'TTLCBM') ||
|
||||
str_contains($norm,'ITLCBM')
|
||||
) {
|
||||
$totalCbm += $val;
|
||||
}
|
||||
|
||||
if (
|
||||
str_contains($norm,'TOTALKG') ||
|
||||
str_contains($norm,'TTKG')
|
||||
) {
|
||||
$totalKg += $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if(!$container->rows->isEmpty())
|
||||
<div class="cm-total-cards-row">
|
||||
<div class="cm-total-card cm-total-card-ctn">
|
||||
<div class="cm-total-icon">
|
||||
<i class="bi bi-box"></i>
|
||||
</div>
|
||||
<div class="cm-total-text">
|
||||
<div class="cm-total-label">Total CTN</div>
|
||||
<div class="cm-total-value">
|
||||
{{ number_format($totalCtn, 0) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cm-total-card cm-total-card-qty">
|
||||
<div class="cm-total-icon">
|
||||
<i class="bi bi-check-circle"></i>
|
||||
</div>
|
||||
<div class="cm-total-text">
|
||||
<div class="cm-total-label">Total QTY</div>
|
||||
<div class="cm-total-value">
|
||||
{{ number_format($totalQty, 0) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cm-total-card cm-total-card-cbm">
|
||||
<div class="cm-total-icon">
|
||||
<i class="bi bi-border-width"></i>
|
||||
</div>
|
||||
<div class="cm-total-text">
|
||||
<div class="cm-total-label">Total CBM</div>
|
||||
<div class="cm-total-value">
|
||||
{{ number_format($totalCbm, 3) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cm-total-card cm-total-card-kg">
|
||||
<div class="cm-total-icon">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
</div>
|
||||
<div class="cm-total-text">
|
||||
<div class="cm-total-label">Total KG</div>
|
||||
<div class="cm-total-value">
|
||||
{{ number_format($totalKg, 2) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($container->rows->isEmpty())
|
||||
<div class="cm-empty">No entries found for this container.</div>
|
||||
@else
|
||||
@php
|
||||
$allHeadings = [];
|
||||
foreach ($container->rows as $row) {
|
||||
if (is_array($row->data)) {
|
||||
$allHeadings = array_unique(array_merge($allHeadings, array_keys($row->data)));
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="cm-filter-bar">
|
||||
<span class="cm-row-count">
|
||||
Total rows: {{ $container->rows->count() }} Edit cells then click "Save Changes".
|
||||
</span>
|
||||
<input type="text" id="cmRowSearch" class="cm-filter-input"
|
||||
placeholder="Quick search..." onkeyup="cmFilterRows()">
|
||||
</div>
|
||||
|
||||
<form id="cm-edit-rows-form" action="{{ route('containers.rows.update', $container->id) }}" method="POST">
|
||||
@csrf
|
||||
<div class="cm-table-scroll-outer">
|
||||
<div class="cm-table-wrapper">
|
||||
<table class="cm-table" id="cmExcelTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
@foreach($allHeadings as $heading)
|
||||
<th>{{ $heading }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($container->rows as $index => $row)
|
||||
<tr>
|
||||
<td class="cm-row-num">{{ $index + 1 }}</td>
|
||||
@foreach($allHeadings as $heading)
|
||||
@php
|
||||
$value = $row->data[$heading] ?? '';
|
||||
|
||||
$norm = strtoupper(str_replace([' ', '/', '-', '.'],'', $heading));
|
||||
|
||||
$isCtn = str_contains($norm, 'CTN');
|
||||
|
||||
$isTotalQty = (
|
||||
str_contains($norm, 'TOTALQTY') ||
|
||||
str_contains($norm, 'ITLQTY') ||
|
||||
str_contains($norm, 'TTLQTY')
|
||||
);
|
||||
$isQty = !$isTotalQty && (
|
||||
str_contains($norm, 'QTY') ||
|
||||
str_contains($norm, 'PCS') ||
|
||||
str_contains($norm, 'PIECES')
|
||||
);
|
||||
|
||||
$isTotalCbm = (
|
||||
str_contains($norm, 'TOTALCBM') ||
|
||||
str_contains($norm, 'TTLCBM') ||
|
||||
str_contains($norm, 'ITLCBM')
|
||||
);
|
||||
$isCbm = !$isTotalCbm && str_contains($norm, 'CBM');
|
||||
|
||||
$isTotalKg = (
|
||||
str_contains($norm, 'TOTALKG') ||
|
||||
str_contains($norm, 'TTKG')
|
||||
);
|
||||
$isKg = !$isTotalKg && (str_contains($norm, 'KG') || str_contains($norm, 'WEIGHT'));
|
||||
|
||||
$isPrice = (str_contains($norm, 'PRICE') || str_contains($norm, 'RATE'));
|
||||
|
||||
$isAmount = (
|
||||
str_contains($norm, 'AMOUNT') ||
|
||||
str_contains($norm, 'TTLAMOUNT') ||
|
||||
str_contains($norm, 'TOTALAMOUNT')
|
||||
);
|
||||
|
||||
$isTotalColumn = $isTotalQty || $isTotalCbm || $isTotalKg || $isAmount;
|
||||
@endphp
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="cm-cell-input {{ $isTotalColumn ? 'cm-cell-readonly' : '' }}"
|
||||
name="rows[{{ $row->id }}][{{ $heading }}]"
|
||||
value="{{ $value }}"
|
||||
data-row-id="{{ $row->id }}"
|
||||
data-col-key="{{ $heading }}"
|
||||
data-ctn="{{ $isCtn ? '1' : '0' }}"
|
||||
data-qty="{{ $isQty ? '1' : '0' }}"
|
||||
data-ttlqty="{{ $isTotalQty ? '1' : '0' }}"
|
||||
data-cbm="{{ $isCbm ? '1' : '0' }}"
|
||||
data-ttlcbm="{{ $isTotalCbm ? '1' : '0' }}"
|
||||
data-kg="{{ $isKg ? '1' : '0' }}"
|
||||
data-ttlkg="{{ $isTotalKg ? '1' : '0' }}"
|
||||
data-price="{{ $isPrice ? '1' : '0' }}"
|
||||
data-amount="{{ $isAmount ? '1' : '0' }}"
|
||||
@if($isTotalColumn) readonly @endif
|
||||
>
|
||||
</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!$container->rows->isEmpty())
|
||||
<button id="cmSaveBtnFloating" class="cm-save-btn-floating">
|
||||
Save Changes
|
||||
</button>
|
||||
<div id="cmToast" class="cm-toast">
|
||||
Changes saved successfully.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<script>
|
||||
function cmFilterRows() {
|
||||
const input = document.getElementById('cmRowSearch');
|
||||
if (!input) return;
|
||||
const filter = input.value.toLowerCase();
|
||||
const table = document.getElementById('cmExcelTable');
|
||||
if (!table) return;
|
||||
const rows = table.getElementsByTagName('tr');
|
||||
for (let i = 1; i < rows.length; i++) {
|
||||
const cells = rows[i].getElementsByTagName('td');
|
||||
let match = false;
|
||||
for (let j = 0; j < cells.length; j++) {
|
||||
const txt = cells[j].textContent || cells[j].innerText;
|
||||
if (txt.toLowerCase().indexOf(filter) > -1) {
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
rows[i].style.display = match ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const form = document.getElementById('cm-edit-rows-form');
|
||||
const btn = document.getElementById('cmSaveBtnFloating');
|
||||
const toast = document.getElementById('cmToast');
|
||||
const table = document.getElementById('cmExcelTable');
|
||||
|
||||
function showToast(message, isError = false) {
|
||||
if (!toast) return;
|
||||
toast.textContent = message;
|
||||
toast.style.background = isError ? '#b91c1c' : '#0f172a';
|
||||
toast.classList.add('cm-show');
|
||||
setTimeout(() => toast.classList.remove('cm-show'), 2500);
|
||||
}
|
||||
|
||||
function parseNumber(str) {
|
||||
if (!str) return 0;
|
||||
const cleaned = String(str).replace(/,/g, '').trim();
|
||||
const val = parseFloat(cleaned);
|
||||
return isNaN(val) ? 0 : val;
|
||||
}
|
||||
|
||||
function formatNumber(val, decimals) {
|
||||
if (isNaN(val)) val = 0;
|
||||
return val.toFixed(decimals);
|
||||
}
|
||||
|
||||
function recalcRow(row) {
|
||||
const inputs = row.querySelectorAll('.cm-cell-input');
|
||||
|
||||
let ctn = 0, qty = 0, ttlQty = 0;
|
||||
let cbm = 0, ttlCbm = 0;
|
||||
let kg = 0, ttlKg = 0;
|
||||
let price = 0, amount = 0;
|
||||
|
||||
let ctnInput = null,
|
||||
qtyInput = null,
|
||||
ttlQtyInput = null,
|
||||
cbmInput = null,
|
||||
ttlCbmInput = null,
|
||||
kgInput = null,
|
||||
ttlKgInput = null,
|
||||
priceInput = null,
|
||||
amountInput = null;
|
||||
|
||||
inputs.forEach(inp => {
|
||||
const val = parseNumber(inp.value);
|
||||
|
||||
if (inp.dataset.ctn === '1') {
|
||||
ctn = val;
|
||||
ctnInput = inp;
|
||||
} else if (inp.dataset.qty === '1') {
|
||||
qty = val;
|
||||
qtyInput = inp;
|
||||
} else if (inp.dataset.ttlqty === '1') {
|
||||
ttlQty = val;
|
||||
ttlQtyInput = inp;
|
||||
} else if (inp.dataset.cbm === '1') {
|
||||
cbm = val;
|
||||
cbmInput = inp;
|
||||
} else if (inp.dataset.ttlcbm === '1') {
|
||||
ttlCbm = val;
|
||||
ttlCbmInput = inp;
|
||||
} else if (inp.dataset.kg === '1') {
|
||||
kg = val;
|
||||
kgInput = inp;
|
||||
} else if (inp.dataset.ttlkg === '1') {
|
||||
ttlKg = val;
|
||||
ttlKgInput = inp;
|
||||
} else if (inp.dataset.price === '1') {
|
||||
price = val;
|
||||
priceInput = inp;
|
||||
} else if (inp.dataset.amount === '1') {
|
||||
amount = val;
|
||||
amountInput = inp;
|
||||
}
|
||||
});
|
||||
|
||||
if (ttlQtyInput && ctnInput && qtyInput) {
|
||||
const newTtlQty = ctn * qty;
|
||||
ttlQtyInput.value = formatNumber(newTtlQty, 0);
|
||||
ttlQty = newTtlQty;
|
||||
}
|
||||
|
||||
if (ttlCbmInput && cbmInput && ctnInput) {
|
||||
const newTtlCbm = cbm * ctn;
|
||||
ttlCbmInput.value = formatNumber(newTtlCbm, 3);
|
||||
ttlCbm = newTtlCbm;
|
||||
}
|
||||
|
||||
if (ttlKgInput && kgInput && ctnInput) {
|
||||
const newTtlKg = kg * ctn;
|
||||
ttlKgInput.value = formatNumber(newTtlKg, 2);
|
||||
ttlKg = newTtlKg;
|
||||
}
|
||||
|
||||
if (amountInput && priceInput && ttlQtyInput) {
|
||||
const newAmount = price * ttlQty;
|
||||
amountInput.value = formatNumber(newAmount, 2);
|
||||
amount = newAmount;
|
||||
}
|
||||
}
|
||||
|
||||
if (table) {
|
||||
table.addEventListener('input', function (e) {
|
||||
const target = e.target;
|
||||
if (!target.classList.contains('cm-cell-input')) return;
|
||||
|
||||
if (target.classList.contains('cm-cell-readonly') || target.hasAttribute('readonly')) {
|
||||
target.blur();
|
||||
return;
|
||||
}
|
||||
|
||||
const row = target.closest('tr');
|
||||
if (row) {
|
||||
recalcRow(row);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (form && btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
btn.classList.add('cm-disabled');
|
||||
const formData = new FormData(form);
|
||||
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-TOKEN': document.querySelector('input[name="_token"]').value
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(async res => {
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text || 'Failed to save');
|
||||
}
|
||||
return res.json().catch(() => ({}));
|
||||
})
|
||||
.then(() => {
|
||||
showToast('Changes saved successfully.');
|
||||
})
|
||||
.catch(() => {
|
||||
showToast('Error while saving changes.', true);
|
||||
})
|
||||
.finally(() => {
|
||||
btn.classList.remove('cm-disabled');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
overflow-x: hidden; /* Prevent horizontal scroll on body */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
@@ -22,7 +22,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* New Stats Container */
|
||||
.stats-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
@@ -43,9 +42,7 @@
|
||||
min-height: 70px;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.stat-card:hover { transform: translateY(-2px); }
|
||||
|
||||
.stat-card.warning {
|
||||
border-left-color: #f59e0b;
|
||||
@@ -57,16 +54,6 @@
|
||||
background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
|
||||
}
|
||||
|
||||
.stat-card.danger {
|
||||
border-left-color: #ef4444;
|
||||
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
|
||||
}
|
||||
|
||||
.stat-card.info {
|
||||
border-left-color: #3b82f6;
|
||||
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
|
||||
}
|
||||
|
||||
.stat-card.secondary {
|
||||
border-left-color: #8b5cf6;
|
||||
background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);
|
||||
@@ -91,46 +78,19 @@
|
||||
.stat-card.warning .stat-icon {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
.stat-card.warning .stat-icon i {
|
||||
color: #f59e0b;
|
||||
}
|
||||
.stat-card.warning .stat-icon i { color: #f59e0b; }
|
||||
|
||||
.stat-card.success .stat-icon {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
.stat-card.success .stat-icon i {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.stat-card.danger .stat-icon {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.stat-card.danger .stat-icon i {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.stat-card.info .stat-icon {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.stat-card.info .stat-icon i {
|
||||
color: #3b82f6;
|
||||
}
|
||||
.stat-card.success .stat-icon i { color: #10b981; }
|
||||
|
||||
.stat-card.secondary .stat-icon {
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
}
|
||||
.stat-card.secondary .stat-icon i { color: #8b5cf6; }
|
||||
|
||||
.stat-card.secondary .stat-icon i {
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
.stat-content { flex: 1; }
|
||||
|
||||
.stat-value {
|
||||
font-size: 22px;
|
||||
@@ -147,14 +107,13 @@
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Updated Search Container - Wider with icon on left */
|
||||
.search-container {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 10px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
width: 350px; /* Increased width */
|
||||
width: 350px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -210,7 +169,6 @@
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Updated Table Styles - Fixed horizontal scroll */
|
||||
.table-glass {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 10px;
|
||||
@@ -219,7 +177,6 @@
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Single gradient for entire header - Blue to Purple */
|
||||
.table thead {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||
}
|
||||
@@ -232,20 +189,9 @@
|
||||
border: none;
|
||||
font-family: 'Inter', sans-serif;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #667eea 0%);;
|
||||
|
||||
background: linear-gradient(135deg, #667eea 0%);
|
||||
}
|
||||
|
||||
/* Remove individual curved borders */
|
||||
.table-header:first-child {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
.table-header:last-child {
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
/* Apply rounded corners to the entire header container */
|
||||
.table-container thead tr:first-child th:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
@@ -254,7 +200,6 @@
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
/* Updated Table Column Alignment */
|
||||
.table > :not(caption) > * > * {
|
||||
padding: 14px 12px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
@@ -263,44 +208,35 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Center align specific columns */
|
||||
.table > :not(caption) > * > *:nth-child(2), /* Customer ID */
|
||||
.table > :not(caption) > * > *:nth-child(3), /* Orders */
|
||||
.table > :not(caption) > * > *:nth-child(4), /* Total */
|
||||
.table > :not(caption) > * > *:nth-child(5) { /* Create Date */
|
||||
.table > :not(caption) > * > *:nth-child(2),
|
||||
.table > :not(caption) > * > *:nth-child(3),
|
||||
.table > :not(caption) > * > *:nth-child(4),
|
||||
.table > :not(caption) > * > *:nth-child(5) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Customer Info column should remain left-aligned */
|
||||
.table > :not(caption) > * > *:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
.table > :not(caption) > * > *:first-child { text-align: left; }
|
||||
|
||||
/* Status and Actions columns should remain as is */
|
||||
.table > :not(caption) > * > *:nth-child(6), /* Status */
|
||||
.table > :not(caption) > * > *:nth-child(7) { /* Actions */
|
||||
.table > :not(caption) > * > *:nth-child(6),
|
||||
.table > :not(caption) > * > *:nth-child(7),
|
||||
.table > :not(caption) > * > *:nth-child(8),
|
||||
.table > :not(caption) > * > *:nth-child(9) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Updated header alignment to match */
|
||||
|
||||
.table-header:nth-child(2),
|
||||
.table-header:nth-child(3),
|
||||
.table-header:nth-child(4),
|
||||
.table-header:nth-child(5) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Customer Info header stays left */
|
||||
.table-header:first-child {
|
||||
text-align: Center;
|
||||
}
|
||||
|
||||
/* Status and Actions headers stay centered */
|
||||
.table-header:nth-child(5),
|
||||
.table-header:nth-child(6),
|
||||
.table-header:nth-child(7) {
|
||||
.table-header:nth-child(7),
|
||||
.table-header:nth-child(8),
|
||||
.table-header:nth-child(9) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table-header:first-child { text-align: Center; }
|
||||
|
||||
.customer-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -374,7 +310,7 @@
|
||||
|
||||
.customer-info-column {
|
||||
min-width: 220px;
|
||||
max-width: 220px; /* Added max-width to prevent overflow */
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.table tbody tr {
|
||||
@@ -391,31 +327,25 @@
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Remove customer-stats since we're adding columns */
|
||||
|
||||
/* Enhanced table styling - Fixed horizontal scroll */
|
||||
.table-container {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
width: 100%; /* Ensure container takes full width */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Fix table responsiveness */
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Ensure table doesn't exceed container */
|
||||
.table {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 0;
|
||||
table-layout: auto; /* Changed to auto for better column distribution */
|
||||
table-layout: auto;
|
||||
}
|
||||
|
||||
/* Fix for search and filter section */
|
||||
.search-filter-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -439,11 +369,10 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* New columns styling */
|
||||
.orders-column, .total-column, .customer-id-column, .create-date-column {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
min-width: 80px; /* Added minimum widths for consistency */
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.orders-count {
|
||||
@@ -458,7 +387,6 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---------- Pagination Styles ---------- */
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -485,14 +413,10 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
.pagination-img-btn {
|
||||
background: #fff;
|
||||
border: 1px solid #e3eaf6;
|
||||
color: #1a2951;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
@@ -500,20 +424,19 @@
|
||||
justify-content: center;
|
||||
min-width: 40px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
.pagination-img-btn:hover:not(:disabled) {
|
||||
background: #1a2951;
|
||||
color: white;
|
||||
border-color: #1a2951;
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
.pagination-img-btn:disabled {
|
||||
background: #f8fafc;
|
||||
color: #cbd5e0;
|
||||
border-color: #e2e8f0;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.pagination-page-btn {
|
||||
@@ -551,61 +474,11 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination-ellipsis {
|
||||
color: #9ba5bb;
|
||||
font-size: 13px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
/* Image-based pagination buttons */
|
||||
.pagination-img-btn {
|
||||
background: #fff;
|
||||
border: 1px solid #e3eaf6;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 40px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pagination-img-btn:hover:not(:disabled) {
|
||||
background: #1a2951;
|
||||
border-color: #1a2951;
|
||||
}
|
||||
|
||||
.pagination-img-btn:disabled {
|
||||
background: #f8fafc;
|
||||
border-color: #e2e8f0;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.pagination-img-btn img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
filter: brightness(0) saturate(100%) invert(26%) sepia(89%) saturate(748%) hue-rotate(201deg) brightness(93%) contrast(89%);
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.pagination-img-btn:hover:not(:disabled) img {
|
||||
filter: brightness(0) saturate(100%) invert(100%) sepia(100%) saturate(0%) hue-rotate(288deg) brightness(106%) contrast(101%);
|
||||
}
|
||||
|
||||
.pagination-img-btn:disabled img {
|
||||
filter: brightness(0) saturate(100%) invert(84%) sepia(8%) saturate(165%) hue-rotate(179deg) brightness(89%) contrast(86%);
|
||||
}
|
||||
|
||||
/* Mobile responsive fixes */
|
||||
@media (max-width: 1200px) {
|
||||
.table > :not(caption) > * > * {
|
||||
padding: 12px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.customer-info-column {
|
||||
min-width: 180px;
|
||||
max-width: 180px;
|
||||
@@ -613,65 +486,42 @@
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.search-container {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.stats-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.search-container { width: 280px; }
|
||||
.stats-container { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.stats-container { grid-template-columns: repeat(2, 1fr); }
|
||||
.search-filter-container {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-section { justify-content: center; }
|
||||
.search-container { width: 100%; }
|
||||
.filter-section {
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pagination-controls { justify-content: center; }
|
||||
.table > :not(caption) > * > * {
|
||||
padding: 10px 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.customer-info-column {
|
||||
min-width: 150px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.customer-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
@@ -680,19 +530,12 @@
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.stats-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stats-container { grid-template-columns: 1fr; }
|
||||
.table-responsive { font-size: 12px; }
|
||||
.customer-info-column {
|
||||
min-width: 120px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.premium-badge,
|
||||
.regular-badge,
|
||||
.status-badge {
|
||||
@@ -703,14 +546,11 @@
|
||||
</style>
|
||||
|
||||
<div class="container-fluid">
|
||||
<!-- Header - Removed gradient -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 style="color: #2c3e50; font-weight: 700; font-family: 'Inter', sans-serif;">Customer List</h4>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards with NEW DESIGN -->
|
||||
<div class="stats-container">
|
||||
<!-- Total Customers -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<i class="bi bi-people-fill"></i>
|
||||
@@ -721,7 +561,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New This Month -->
|
||||
<div class="stat-card warning">
|
||||
<div class="stat-icon">
|
||||
<i class="bi bi-person-plus"></i>
|
||||
@@ -739,7 +578,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Customers -->
|
||||
<div class="stat-card success">
|
||||
<div class="stat-icon">
|
||||
<i class="bi bi-activity"></i>
|
||||
@@ -755,7 +593,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Premium Customers -->
|
||||
<div class="stat-card secondary">
|
||||
<div class="stat-icon">
|
||||
<i class="bi bi-award-fill"></i>
|
||||
@@ -772,10 +609,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter Section -->
|
||||
<div class="glass-card p-3 mb-3">
|
||||
<div class="search-filter-container">
|
||||
<!-- Search Section - Wider with icon on left -->
|
||||
<div class="search-section">
|
||||
<form method="GET" action="{{ route('admin.customers.index') }}" class="d-flex align-items-center">
|
||||
<div class="search-container">
|
||||
@@ -792,7 +627,6 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Filter Section -->
|
||||
<div class="filter-section">
|
||||
<a href="{{ route('admin.customers.index', ['status'=>'active', 'search'=>$search ?? '']) }}"
|
||||
class="filter-btn {{ ($status ?? '') == 'active' ? 'active' : '' }}">
|
||||
@@ -818,7 +652,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customer List Table -->
|
||||
<div class="table-container">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
@@ -827,86 +660,118 @@
|
||||
<th class="table-header">Customer Info</th>
|
||||
<th class="table-header">Customer ID</th>
|
||||
<th class="table-header">Orders</th>
|
||||
<th class="table-header">Total</th>
|
||||
<th class="table-header">Order Total</th>
|
||||
<th class="table-header">Total Payable</th>
|
||||
<th class="table-header">Remaining</th>
|
||||
<th class="table-header">Create Date</th>
|
||||
<th class="table-header">Status</th>
|
||||
<th class="table-header" width="100">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody id="customersTableBody">
|
||||
@forelse($customers as $c)
|
||||
<tr>
|
||||
<!-- Customer Info Column -->
|
||||
<td class="customer-info-column">
|
||||
<div class="d-flex align-items-start">
|
||||
<div class="customer-avatar me-3">
|
||||
{{ strtoupper(substr($c->customer_name,0,1)) }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold">{{ $c->customer_name }}</div>
|
||||
@if($c->customer_type == 'premium')
|
||||
<span class="premium-badge">Premium Customer</span>
|
||||
@else
|
||||
<span class="regular-badge">Regular Customer</span>
|
||||
@endif
|
||||
<div class="customer-details mt-1">
|
||||
{{ $c->email }}<br>
|
||||
{{ $c->mobile_no }}
|
||||
@php
|
||||
// Orders = invoice count
|
||||
$ordersCount = $c->invoices->count();
|
||||
|
||||
// Order Total = items total from all invoices (final_amount)
|
||||
$orderTotal = $c->invoices->sum('final_amount');
|
||||
|
||||
// Total payable = grand total with GST + groups
|
||||
$totalPayable = $c->invoices->sum('grand_total_with_charges');
|
||||
|
||||
// Total paid via installments
|
||||
$totalPaid = $c->invoiceInstallments->sum('amount');
|
||||
|
||||
// Remaining amount
|
||||
$remainingAmount = max($totalPayable - $totalPaid, 0);
|
||||
@endphp
|
||||
<tr>
|
||||
<td class="customer-info-column">
|
||||
<div class="d-flex align-items-start">
|
||||
<div class="customer-avatar me-3">
|
||||
{{ strtoupper(substr($c->customer_name,0,1)) }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold">{{ $c->customer_name }}</div>
|
||||
@if($c->customer_type == 'premium')
|
||||
<span class="premium-badge">Premium Customer</span>
|
||||
@else
|
||||
<span class="regular-badge">Regular Customer</span>
|
||||
@endif
|
||||
<div class="customer-details mt-1">
|
||||
{{ $c->email }}<br>
|
||||
{{ $c->mobile_no }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</td>
|
||||
|
||||
<!-- Customer ID -->
|
||||
<td class="customer-id-column">
|
||||
<span class="fw-bold text-primary">{{ $c->customer_id }}</span>
|
||||
</td>
|
||||
<td class="customer-id-column">
|
||||
<span class="fw-bold text-primary">{{ $c->customer_id }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Orders Column -->
|
||||
<td class="orders-column">
|
||||
<span class="orders-count">{{ $c->orders->count() }}</span>
|
||||
</td>
|
||||
<td class="orders-column">
|
||||
<span class="orders-count">{{ $ordersCount }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Total Column -->
|
||||
<td class="total-column">
|
||||
<span class="total-amount">₹{{ number_format($c->orders->sum('ttl_amount'), 2) }}</span>
|
||||
</td>
|
||||
<td class="total-column">
|
||||
<span class="total-amount">
|
||||
₹{{ number_format($orderTotal, 2) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Create Date -->
|
||||
<td class="create-date-column">
|
||||
<span class="text-muted">{{ $c->created_at ? $c->created_at->format('d-m-Y') : '-' }}</span>
|
||||
</td>
|
||||
<td class="total-column">
|
||||
<span class="total-amount">
|
||||
₹{{ number_format($totalPayable, 2) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Status -->
|
||||
<td>
|
||||
@if($c->status === 'active')
|
||||
<span class="status-badge active-status">Active</span>
|
||||
@else
|
||||
<span class="status-badge inactive-status">Inactive</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="total-column">
|
||||
@if($remainingAmount > 0)
|
||||
<span class="text-danger fw-bold">
|
||||
₹{{ number_format($remainingAmount, 2) }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-success fw-bold">
|
||||
₹0.00
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<!-- Actions -->
|
||||
<td>
|
||||
<div class="d-flex justify-content-center">
|
||||
<a href="{{ route('admin.customers.view', $c->id) }}"
|
||||
class="action-btn" title="View">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<td class="create-date-column">
|
||||
<span class="text-muted">{{ $c->created_at ? $c->created_at->format('d-m-Y') : '-' }}</span>
|
||||
</td>
|
||||
|
||||
<form action="{{ route('admin.customers.status', $c->id) }}"
|
||||
method="POST" style="display:inline-block;">
|
||||
@csrf
|
||||
<button class="action-btn" title="Toggle Status" type="submit">
|
||||
<i class="bi bi-power"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<td>
|
||||
@if($c->status === 'active')
|
||||
<span class="status-badge active-status">Active</span>
|
||||
@else
|
||||
<span class="status-badge inactive-status">Inactive</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="d-flex justify-content-center">
|
||||
<a href="{{ route('admin.customers.view', $c->id) }}"
|
||||
class="action-btn" title="View">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
|
||||
<form action="{{ route('admin.customers.status', $c->id) }}"
|
||||
method="POST" style="display:inline-block;">
|
||||
@csrf
|
||||
<button class="action-btn" title="Toggle Status" type="submit">
|
||||
<i class="bi bi-power"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4">
|
||||
<td colspan="9" class="text-center py-4">
|
||||
<i class="bi bi-people display-4 text-muted d-block mb-2"></i>
|
||||
<span class="text-muted">No customers found.</span>
|
||||
</td>
|
||||
@@ -917,14 +782,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
<div class="pagination-container">
|
||||
<div class="pagination-info" id="pageInfo">
|
||||
Showing {{ $customers->firstItem() ?? 0 }} to {{ $customers->lastItem() ?? 0 }} of {{ $customers->total() }} entries
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<button class="pagination-img-btn" id="prevPageBtn" title="Previous page" {{ $customers->onFirstPage() ? 'disabled' : '' }}>
|
||||
<!-- Left arrow SVG -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 12L6 8L10 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
@@ -938,7 +801,6 @@
|
||||
@endfor
|
||||
</div>
|
||||
<button class="pagination-img-btn" id="nextPageBtn" title="Next page" {{ $customers->hasMorePages() ? '' : 'disabled' }}>
|
||||
<!-- Right arrow SVG -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 4L10 8L6 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
@@ -949,7 +811,6 @@
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add hover effects to table rows
|
||||
const tableRows = document.querySelectorAll('.table tbody tr');
|
||||
tableRows.forEach(row => {
|
||||
row.addEventListener('mouseenter', function() {
|
||||
@@ -961,7 +822,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Pagination button handlers
|
||||
document.getElementById('prevPageBtn').addEventListener('click', function() {
|
||||
@if(!$customers->onFirstPage())
|
||||
window.location.href = '{{ $customers->previousPageUrl() }}';
|
||||
@@ -976,4 +836,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
@endsection
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@
|
||||
@section('page-title', 'Customer Details')
|
||||
|
||||
@section('content')
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
@@ -33,10 +32,12 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 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%);
|
||||
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%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -81,10 +82,12 @@
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: linear-gradient(45deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.1) 50%,
|
||||
transparent 100%);
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.1) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
animation: headerShimmer 8s infinite linear;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
@@ -333,7 +336,7 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Buttons - FIXED POSITIONING */
|
||||
/* Buttons */
|
||||
.btn-back {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
@@ -358,10 +361,12 @@
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.2),
|
||||
transparent);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.2),
|
||||
transparent
|
||||
);
|
||||
transition: left 0.6s ease;
|
||||
}
|
||||
|
||||
@@ -397,10 +402,12 @@
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.2),
|
||||
transparent);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.2),
|
||||
transparent
|
||||
);
|
||||
transition: left 0.6s ease;
|
||||
}
|
||||
|
||||
@@ -497,7 +504,7 @@
|
||||
.animation-delay-3 { animation-delay: 0.3s; }
|
||||
.animation-delay-4 { animation-delay: 0.4s; }
|
||||
|
||||
/* Header Button Container - FIXED */
|
||||
/* Header Button Container */
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -511,47 +518,48 @@
|
||||
padding: 25px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.customer-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
|
||||
.page-header {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
.stats-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
.stats-value {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
|
||||
.header-actions {
|
||||
justify-content: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
|
||||
.page-header .row {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.btn-back, .btn-outline-secondary {
|
||||
.btn-back,
|
||||
.btn-outline-secondary {
|
||||
padding: 10px 20px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
|
||||
.header-actions {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.header-actions a {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
@@ -560,8 +568,7 @@
|
||||
</style>
|
||||
|
||||
<div class="container py-4">
|
||||
|
||||
{{-- HEADER - FIXED BUTTON POSITION --}}
|
||||
{{-- HEADER --}}
|
||||
<div class="page-header animate-fade-in">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
@@ -586,7 +593,7 @@
|
||||
<div class="col-auto">
|
||||
<div class="avatar-container">
|
||||
<div class="customer-avatar">
|
||||
{{ strtoupper(substr($customer->customer_name,0,1)) }}
|
||||
{{ strtoupper(substr($customer->customer_name, 0, 1)) }}
|
||||
</div>
|
||||
<div class="avatar-status {{ $customer->status == 'active' ? 'status-active' : 'status-inactive' }}"></div>
|
||||
</div>
|
||||
@@ -698,11 +705,33 @@
|
||||
<div class="stats-icon">
|
||||
<i class="bi bi-currency-rupee"></i>
|
||||
</div>
|
||||
<div class="stats-value">₹{{ number_format($totalAmount, 2) }}</div>
|
||||
<div class="stats-value">₹{{ number_format($totalOrderAmount, 2) }}</div>
|
||||
<div class="stats-label">Total Amount Spent</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Total Payable --}}
|
||||
<div class="col-md-4 animate-fade-in animation-delay-3">
|
||||
<div class="stats-card marks">
|
||||
<div class="stats-icon">
|
||||
<i class="bi bi-wallet2"></i>
|
||||
</div>
|
||||
<div class="stats-value">₹{{ number_format($totalPayable, 2) }}</div>
|
||||
<div class="stats-label">Total Payable</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Remaining Amount --}}
|
||||
<div class="col-md-4 animate-fade-in animation-delay-3">
|
||||
<div class="stats-card marks">
|
||||
<div class="stats-icon">
|
||||
<i class="bi bi-exclamation-circle"></i>
|
||||
</div>
|
||||
<div class="stats-value">₹{{ number_format($totalRemaining, 2) }}</div>
|
||||
<div class="stats-label">Remaining Amount</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Mark Count --}}
|
||||
<div class="col-md-4 animate-fade-in animation-delay-3">
|
||||
<div class="stats-card marks">
|
||||
@@ -724,7 +753,7 @@
|
||||
<span class="badge bg-primary ms-2">{{ $customer->marks->count() }}</span>
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="section-body">
|
||||
@if($customer->marks->count() == 0)
|
||||
<div class="text-center py-5">
|
||||
@@ -746,20 +775,23 @@
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Add hover effects to interactive elements
|
||||
const interactiveElements = document.querySelectorAll('.info-card, .stats-card, .mark-item');
|
||||
|
||||
|
||||
interactiveElements.forEach(element => {
|
||||
element.addEventListener('mouseenter', function() {
|
||||
this.style.transform = this.classList.contains('mark-item') ? 'translateX(5px)' : 'translateY(-5px)';
|
||||
element.addEventListener('mouseenter', function () {
|
||||
if (this.classList.contains('mark-item')) {
|
||||
this.style.transform = 'translateX(5px)';
|
||||
} else {
|
||||
this.style.transform = 'translateY(-5px)';
|
||||
}
|
||||
});
|
||||
|
||||
element.addEventListener('mouseleave', function() {
|
||||
|
||||
element.addEventListener('mouseleave', function () {
|
||||
this.style.transform = 'translateY(0)';
|
||||
});
|
||||
});
|
||||
@@ -769,7 +801,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
statsValues.forEach(value => {
|
||||
const originalText = value.textContent;
|
||||
value.textContent = '0';
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
value.textContent = originalText;
|
||||
value.style.transform = 'scale(1.1)';
|
||||
@@ -780,5 +812,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
@endsection
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,10 +8,10 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
/*Remove horizontal scroll bar*/
|
||||
html, body {
|
||||
/* html, body {
|
||||
overflow-x: hidden !important;
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
}
|
||||
} */
|
||||
|
||||
/* Invoice Management Styles */
|
||||
.invoice-management-box {
|
||||
@@ -27,7 +27,7 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 17px 17px 0 0;
|
||||
background: #fceeb8ff;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 54px;
|
||||
padding: 15px 26px 10px 22px;
|
||||
border-bottom: 1.4px solid #e8e2cf;
|
||||
@@ -37,7 +37,7 @@
|
||||
.invoice-management-title {
|
||||
font-size: 1.32rem;
|
||||
font-weight: 800;
|
||||
color: #2451af;
|
||||
color: #ffffffff;
|
||||
letter-spacing: .08em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -50,43 +50,50 @@
|
||||
color: #336ad3;
|
||||
}
|
||||
|
||||
/* Tools Row Styling */
|
||||
/* ===== UPDATED FILTER BAR WITH DATE RANGE ===== */
|
||||
.invoice-tools-row {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #edf2f7 100%);
|
||||
padding: 25px 30px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
box-shadow: 0 10px 25px -5px rgba(99, 102, 241, 0.3);
|
||||
position: relative;
|
||||
border-radius: 17px 17px 0 0;
|
||||
top:5px;
|
||||
}
|
||||
|
||||
.filter-bar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Search Box Styling */
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 10px 18px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.08);
|
||||
border: 1.5px solid #e2e8f0;
|
||||
min-width: 380px;
|
||||
flex: 1;
|
||||
max-width: 500px;
|
||||
border-radius: 50px;
|
||||
padding: 8px 20px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.08), inset 0 1px 2px rgba(255,255,255,0.5);
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
flex: 2;
|
||||
min-width: 250px;
|
||||
transition: all 0.3s ease;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.search-box:focus-within {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 4px 20px rgba(59, 130, 246, 0.15);
|
||||
box-shadow: 0 8px 20px rgba(79, 70, 229, 0.25);
|
||||
transform: translateY(-2px);
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.search-box i {
|
||||
color: #64748b;
|
||||
color: #667eea;
|
||||
margin-right: 12px;
|
||||
font-size: 1.2rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
@@ -95,93 +102,189 @@
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
color: #334155;
|
||||
color: #1f2937;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.search-box input::placeholder {
|
||||
color: #94a3b8;
|
||||
color: #9ca3af;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* .search-btn {
|
||||
background: linear-gradient(90deg, #226ad6, #46b4fd 123%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 5px 10px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: 3px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
white-space: nowrap;
|
||||
} */
|
||||
|
||||
.search-btn:hover {
|
||||
background: linear-gradient(90deg, #3264f8, #3acfff 140%);
|
||||
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
/* Filter Selects Styling */
|
||||
.filter-select-wrapper {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
width: 100%;
|
||||
background: white;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
padding: 10px 15px;
|
||||
border: 1px solid rgba(255,255,255,0.5);
|
||||
border-radius: 50px;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
color: #1f2937;
|
||||
outline: none;
|
||||
min-width: 160px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.05);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 400;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23667eea' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 15px center;
|
||||
background-size: 16px;
|
||||
}
|
||||
|
||||
.filter-select:hover {
|
||||
background-color: white;
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.filter-select:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
border-color: white;
|
||||
box-shadow: 0 0 0 3px rgba(255,255,255,0.2);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.create-invoice-btn {
|
||||
background: linear-gradient(90deg, #226ad6, #46b4fd 123%);
|
||||
padding: 12px 28px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
box-shadow: 0 2px 13px #dde7fa42;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
/* ===== NEW: Status dropdown option colors ===== */
|
||||
.filter-select option[value="paid"] {
|
||||
background-color: #d1fae5;
|
||||
color: #065f46;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-select option[value="pending"] {
|
||||
background-color: #fef3c7;
|
||||
color: #d97706;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-select option[value="overdue"] {
|
||||
background-color: #e9d5ff;
|
||||
color: #6b21a8;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-select option[value="all"] {
|
||||
background-color: white;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* For Firefox compatibility */
|
||||
.filter-select option {
|
||||
padding: 8px;
|
||||
}
|
||||
/* ===== END status dropdown option colors ===== */
|
||||
|
||||
/* Date Range Styling */
|
||||
.date-range-wrapper {
|
||||
flex: 1.5;
|
||||
min-width: 280px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
/* background: rgba(255, 255, 255, 0.2); */
|
||||
padding: 5px 12px;
|
||||
border-radius: 50px;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.date-input-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 50px;
|
||||
padding: 5px 12px;
|
||||
flex: 1;
|
||||
border: 1px solid rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
.date-input-container i {
|
||||
color: #667eea;
|
||||
margin-right: 6px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
color: #1f2937;
|
||||
font-family: 'Inter', sans-serif;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.create-invoice-btn:hover {
|
||||
background: linear-gradient(90deg, #3264f8, #3acfff 140%);
|
||||
box-shadow: 0 4px 25px #5ab8f880;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
transform: translateY(-2px);
|
||||
.date-input::placeholder {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1100px) {
|
||||
.filter-bar-container {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.date-range-wrapper {
|
||||
min-width: 260px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.invoice-tools-row {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.filter-bar-container {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.filter-select-wrapper {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.date-range-wrapper {
|
||||
min-width: 100%;
|
||||
flex-direction: column;
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
padding: 0;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-input-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
/* ===== END UPDATED FILTER BAR ===== */
|
||||
|
||||
.invoice-management-main {
|
||||
background: #fff;
|
||||
border-radius: 0 0 17px 17px;
|
||||
@@ -223,7 +326,7 @@
|
||||
|
||||
/* Center all table content */
|
||||
.table thead tr {
|
||||
background: #feebbe !important;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.table thead th:first-child {
|
||||
@@ -237,7 +340,7 @@
|
||||
background: transparent !important;
|
||||
border: none;
|
||||
font-weight: 700;
|
||||
color: #343535;
|
||||
color: #ffffffff;
|
||||
letter-spacing: 0.02em;
|
||||
font-size: 14px;
|
||||
padding: 20px 15px;
|
||||
@@ -258,25 +361,33 @@
|
||||
|
||||
/* Soft blue background for ALL table rows */
|
||||
.table-striped tbody tr {
|
||||
background: #f0f8ff !important;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
background: #f0f8ff !important;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
|
||||
.table-striped tbody tr td {
|
||||
padding: 6px 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.table-striped tbody tr:hover {
|
||||
background: #e6f3ff !important;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
||||
transform: translateY(-0.5px);
|
||||
}
|
||||
|
||||
.table-striped tbody tr:hover {
|
||||
background: #e6f3ff !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Remove striped pattern - all rows same soft blue */
|
||||
.table-striped tbody tr:nth-of-type(odd),
|
||||
.table-striped tbody tr:nth-of-type(even) {
|
||||
background: #f0f8ff !important;
|
||||
}
|
||||
|
||||
/* Center all table cells with proper spacing */
|
||||
|
||||
.table td {
|
||||
padding: 18px 15px;
|
||||
border: none;
|
||||
@@ -291,7 +402,7 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* First and last cell rounded corners */
|
||||
|
||||
.table td:first-child {
|
||||
padding-left: 30px;
|
||||
font-weight: 600;
|
||||
@@ -561,37 +672,6 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Date Range Picker Styles */
|
||||
.date-range-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
background: white;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
outline: none;
|
||||
min-width: 140px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.04);
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.date-input:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Stats Summary - Centered */
|
||||
.stats-summary {
|
||||
display: grid;
|
||||
@@ -942,17 +1022,6 @@
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1200px) {
|
||||
.invoice-tools-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
max-width: 100%;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
@@ -970,10 +1039,6 @@
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.invoice-tools-row {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.table th, .table td {
|
||||
font-size: 13px;
|
||||
padding: 15px 10px;
|
||||
@@ -995,21 +1060,6 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
min-width: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.create-invoice-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.invoice-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
@@ -1025,15 +1075,6 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.date-range-container {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stats-summary {
|
||||
grid-template-columns: 1fr;
|
||||
margin: 20px;
|
||||
@@ -1071,11 +1112,6 @@
|
||||
min-width: auto;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
padding: 8px 15px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1088,39 +1124,58 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- TOOLS ROW - Search, Filter, Create Button -->
|
||||
<!-- UPDATED FILTER BAR WITH DATE RANGE -->
|
||||
<div class="invoice-tools-row">
|
||||
<!-- Search Box with Button -->
|
||||
<div style="display: flex; align-items: center; flex: 1; max-width: 500px; min-width: 380px;">
|
||||
<div class="search-box">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text" id="invoiceSearch" placeholder="Search by invoice number, customer name...">
|
||||
<form method="GET" action="{{ route('admin.invoices.index') }}" style="width: 100%;">
|
||||
<div class="filter-bar-container">
|
||||
<!-- Search Box -->
|
||||
<div class="search-box">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text"
|
||||
id="searchInput"
|
||||
name="search"
|
||||
value="{{ request('search') }}"
|
||||
placeholder="Search Invoices...">
|
||||
</div>
|
||||
|
||||
<!-- Status Filter -->
|
||||
<div class="filter-select-wrapper">
|
||||
<select class="filter-select" id="statusFilter" name="status">
|
||||
<option value="all" {{ request('status')=='all' ? 'selected' : '' }}>All Status</option>
|
||||
<option value="paid" {{ request('status')=='paid' ? 'selected' : '' }}>Paid</option>
|
||||
<option value="pending" {{ request('status')=='pending' ? 'selected' : '' }}>Pending</option>
|
||||
<option value="overdue" {{ request('status')=='overdue' ? 'selected' : '' }}>Overdue</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter (FROM - TO) with dd-mm-yyyy format -->
|
||||
<div class="date-range-wrapper">
|
||||
<div class="date-input-container">
|
||||
<i class="bi bi-calendar3"></i>
|
||||
<input type="text"
|
||||
class="date-input"
|
||||
id="startDate"
|
||||
name="start_date"
|
||||
value="{{ request('start_date') ? date('d-m-Y', strtotime(request('start_date'))) : '' }}"
|
||||
placeholder="dd-mm-yyyy"
|
||||
onfocus="(this.type='date')"
|
||||
onblur="if(!this.value) this.type='text'">
|
||||
</div>
|
||||
<span class="date-separator">to</span>
|
||||
<div class="date-input-container">
|
||||
<i class="bi bi-calendar3"></i>
|
||||
<input type="text"
|
||||
class="date-input"
|
||||
id="endDate"
|
||||
name="end_date"
|
||||
value="{{ request('end_date') ? date('d-m-Y', strtotime(request('end_date'))) : '' }}"
|
||||
placeholder="dd-mm-yyyy"
|
||||
onfocus="(this.type='date')"
|
||||
onblur="if(!this.value) this.type='text'">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filter Group -->
|
||||
<div class="filter-group">
|
||||
<select class="filter-select" id="statusFilter">
|
||||
<option value="">All Status</option>
|
||||
<option value="paid">Paid</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="overdue">Overdue</option>
|
||||
</select>
|
||||
|
||||
<!-- Date Range Picker -->
|
||||
<div class="date-range-container">
|
||||
<input type="date" class="date-input" id="startDate">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date" class="date-input" id="endDate">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Invoice Button -->
|
||||
<!-- <a href="{{ route('admin.invoices.create') }}" class="create-invoice-btn">
|
||||
<i class="bi bi-plus-circle"></i> Create Invoice
|
||||
</a> -->
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="invoice-management-main no-extra-space">
|
||||
@@ -1156,77 +1211,107 @@
|
||||
<div class="card-body p-0">
|
||||
<div class="table-container">
|
||||
<table class="table table-striped align-middle" id="invoicesTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="column-header">#</th>
|
||||
<th class="column-header">Invoice Number</th>
|
||||
<th class="column-header">Customer</th>
|
||||
<th class="column-header">Final Amount</th>
|
||||
<th class="column-header">GST %</th>
|
||||
<th class="column-header">Total w/GST</th>
|
||||
<th class="column-header">Status</th>
|
||||
<th class="column-header">Invoice Date</th>
|
||||
<th class="column-header">Due Date</th>
|
||||
<th class="column-header">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="column-header">#</th>
|
||||
<th class="column-header">Invoice Number</th>
|
||||
<th class="column-header">Customer</th>
|
||||
<th class="column-header">Container</th> {{-- NEW --}}
|
||||
<th class="column-header">Final Amount</th>
|
||||
<th class="column-header">GST %</th>
|
||||
<th class="column-header">Total w/GST</th>
|
||||
<th class="column-header">Status</th>
|
||||
<th class="column-header">Invoice Date</th>
|
||||
<th class="column-header">Due Date</th>
|
||||
<th class="column-header">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody id="invoicesTableBody">
|
||||
@php
|
||||
$totalInvoices = $invoices->count();
|
||||
$sortedInvoices = $invoices->sortByDesc('created_at'); // Latest first
|
||||
@endphp
|
||||
|
||||
@forelse($sortedInvoices as $i => $invoice)
|
||||
<tr>
|
||||
<td>{{ $totalInvoices - $i }}</td>
|
||||
<tbody id="invoicesTableBody">
|
||||
@php
|
||||
$totalInvoices = $invoices->count();
|
||||
$sortedInvoices = $invoices->sortByDesc('created_at'); // Latest first
|
||||
@endphp
|
||||
|
||||
@forelse($sortedInvoices as $i => $invoice)
|
||||
<tr>
|
||||
<td>{{ $totalInvoices - $i }}</td>
|
||||
|
||||
<td>
|
||||
<div class="invoice-number-cell">
|
||||
<div class="invoice-icon invoice-icon-{{ (($totalInvoices - $i) % 8) + 1 }}">
|
||||
<i class="bi bi-file-earmark-text"></i>
|
||||
</div>
|
||||
<a href="#" class="invoice-number-link open-invoice-popup" data-id="{{ $invoice->id }}">
|
||||
{{ $invoice->invoice_number }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="invoice-number-cell">
|
||||
<div class="invoice-icon invoice-icon-{{ (($totalInvoices - $i) % 8) + 1 }}">
|
||||
<i class="bi bi-file-earmark-text"></i>
|
||||
</div>
|
||||
<a href="#" class="invoice-number-link open-invoice-popup" data-id="{{ $invoice->id }}">
|
||||
{{ $invoice->invoice_number }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="customer-cell">{{ $invoice->customer_name }}</td>
|
||||
<td class="customer-cell">
|
||||
{{ $invoice->customer_name }}
|
||||
</td>
|
||||
|
||||
<td class="amount-cell">₹{{ number_format($invoice->final_amount, 2) }}</td>
|
||||
<td class="gst-cell">{{ $invoice->gst_percent }}%</td>
|
||||
<td class="amount-cell">₹{{ number_format($invoice->final_amount_with_gst, 2) }}</td>
|
||||
{{-- NEW: Container column --}}
|
||||
<td class="customer-cell">
|
||||
@if($invoice->container)
|
||||
{{ $invoice->container->container_number }}
|
||||
{{-- जर फक्त ID हवी असेल तर:
|
||||
#{{ $invoice->container->id }}
|
||||
--}}
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span class="badge badge-{{ $invoice->status }}">
|
||||
@if($invoice->status == 'paid')
|
||||
<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 == 'overdue')
|
||||
<i class="bi bi-exclamation-triangle-fill status-icon"></i>
|
||||
@endif
|
||||
{{ ucfirst($invoice->status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="amount-cell">
|
||||
₹{{ number_format($invoice->final_amount, 2) }}
|
||||
</td>
|
||||
|
||||
<td class="date-cell">{{ $invoice->invoice_date }}</td>
|
||||
<td class="date-cell">{{ $invoice->due_date }}</td>
|
||||
<td class="gst-cell">
|
||||
{{ $invoice->gst_percent }}%
|
||||
</td>
|
||||
|
||||
<td class="amount-cell">
|
||||
₹{{ number_format($invoice->final_amount_with_gst, 2) }}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span class="badge badge-{{ $invoice->status }}">
|
||||
@if($invoice->status == 'paid')
|
||||
<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 == 'overdue')
|
||||
<i class="bi bi-exclamation-triangle-fill status-icon"></i>
|
||||
@endif
|
||||
{{ ucfirst($invoice->status) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="date-cell">
|
||||
{{ $invoice->invoice_date }}
|
||||
</td>
|
||||
|
||||
<td class="date-cell">
|
||||
{{ $invoice->due_date }}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="{{ route('admin.invoices.edit', $invoice->id) }}"
|
||||
class="btn-entry">
|
||||
<i class="bi bi-pencil"></i> Entry
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
{{-- 1 new column वाढवलाय म्हणून colspan 11 --}}
|
||||
<td colspan="11" class="text-muted">No invoices found</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
|
||||
<td>
|
||||
<a href="{{ route('admin.invoices.edit', $invoice->id) }}"
|
||||
class="btn-entry">
|
||||
<i class="bi bi-pencil"></i> Entry
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="10" class="text-muted">No invoices found</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1341,7 +1426,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Pagination state
|
||||
@@ -1388,18 +1472,28 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
// Search and filter functionality for both desktop and mobile
|
||||
const searchInput = document.getElementById('invoiceSearch');
|
||||
// Function to parse dd-mm-yyyy to Date object
|
||||
function parseDate(dateStr) {
|
||||
if (!dateStr) return null;
|
||||
const parts = dateStr.split('-');
|
||||
if (parts.length === 3) {
|
||||
// dd-mm-yyyy format
|
||||
return new Date(parts[2], parts[1] - 1, parts[0]);
|
||||
}
|
||||
return new Date(dateStr);
|
||||
}
|
||||
|
||||
// Search and filter functionality
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const statusFilter = document.getElementById('statusFilter');
|
||||
const startDateInput = document.getElementById('startDate');
|
||||
const endDateInput = document.getElementById('endDate');
|
||||
const searchButton = document.getElementById('searchButton');
|
||||
const startDate = document.getElementById('startDate');
|
||||
const endDate = document.getElementById('endDate');
|
||||
|
||||
function filterInvoices() {
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
const statusValue = statusFilter.value;
|
||||
const startDate = startDateInput.value;
|
||||
const endDate = endDateInput.value;
|
||||
const startDateValue = startDate.value;
|
||||
const endDateValue = endDate.value;
|
||||
|
||||
filteredInvoices = allInvoices.filter(invoice => {
|
||||
let include = true;
|
||||
@@ -1413,18 +1507,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (statusValue && invoice.status !== statusValue) {
|
||||
if (statusValue && statusValue !== 'all' && invoice.status !== statusValue) {
|
||||
include = false;
|
||||
}
|
||||
|
||||
// Date filter
|
||||
if (startDate || endDate) {
|
||||
const invoiceDate = new Date(invoice.invoice_date);
|
||||
const start = startDate ? new Date(startDate) : null;
|
||||
const end = endDate ? new Date(endDate) : null;
|
||||
// Date range filter
|
||||
if (startDateValue || endDateValue) {
|
||||
const invoiceDate = parseDate(invoice.invoice_date);
|
||||
|
||||
if (start && invoiceDate < start) include = false;
|
||||
if (end && invoiceDate > end) include = false;
|
||||
if (startDateValue) {
|
||||
const start = parseDate(startDateValue);
|
||||
if (invoiceDate < start) include = false;
|
||||
}
|
||||
|
||||
if (endDateValue) {
|
||||
const end = parseDate(endDateValue);
|
||||
if (invoiceDate > end) include = false;
|
||||
}
|
||||
}
|
||||
|
||||
return include;
|
||||
@@ -1437,14 +1536,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Add event listeners for filtering
|
||||
searchInput.addEventListener('input', filterInvoices);
|
||||
searchButton.addEventListener('click', filterInvoices);
|
||||
statusFilter.addEventListener('change', filterInvoices);
|
||||
startDateInput.addEventListener('change', filterInvoices);
|
||||
endDateInput.addEventListener('change', filterInvoices);
|
||||
|
||||
startDate.addEventListener('change', filterInvoices);
|
||||
endDate.addEventListener('change', filterInvoices);
|
||||
|
||||
// Also trigger search on Enter key in search input
|
||||
searchInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
filterInvoices();
|
||||
}
|
||||
});
|
||||
@@ -1534,7 +1633,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const mobileContainer = document.getElementById('mobileInvoicesContainer');
|
||||
|
||||
if (filteredInvoices.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="10" class="text-muted">No invoices found</td></tr>';
|
||||
// 1 column वाढवल्यामुळे colspan 11
|
||||
tbody.innerHTML = '<tr><td colspan="11" class="text-muted">No invoices found</td></tr>';
|
||||
mobileContainer.innerHTML = '<div class="text-muted text-center py-4">No invoices found</div>';
|
||||
return;
|
||||
}
|
||||
@@ -1565,6 +1665,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</div>
|
||||
</td>
|
||||
<td class="customer-cell">${invoice.customer_name}</td>
|
||||
<!-- NEW: Container column -->
|
||||
<td class="customer-cell">
|
||||
${invoice.container ? (invoice.container.container_number ?? '—') : '—'}
|
||||
</td>
|
||||
<td class="amount-cell">₹${parseFloat(invoice.final_amount).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</td>
|
||||
<td class="gst-cell">${invoice.gst_percent}%</td>
|
||||
<td class="amount-cell">₹${parseFloat(invoice.final_amount_with_gst).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</td>
|
||||
@@ -1614,6 +1718,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<span class="mobile-detail-label">Customer</span>
|
||||
<span class="mobile-detail-value">${invoice.customer_name}</span>
|
||||
</div>
|
||||
<!-- NEW: Container for mobile -->
|
||||
<div class="mobile-detail-item">
|
||||
<span class="mobile-detail-label">Container</span>
|
||||
<span class="mobile-detail-value">
|
||||
${invoice.container ? (invoice.container.container_number ?? '—') : '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mobile-detail-item">
|
||||
<span class="mobile-detail-label">Amount</span>
|
||||
<span class="mobile-detail-value">₹${parseFloat(invoice.final_amount).toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</span>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>Admin Panel</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet" />
|
||||
@@ -199,7 +201,7 @@
|
||||
background: #fff;
|
||||
padding: 10px 18px !important;
|
||||
position: relative;
|
||||
height: 48px;
|
||||
height: 65px;
|
||||
width: 100%;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
@@ -219,7 +221,34 @@
|
||||
font-size: 1.06rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
HEADER NOTIFICATION BADGE FIX
|
||||
================================ */
|
||||
|
||||
/* Target ONLY badge inside bell icon */
|
||||
header .bi-bell {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Override broken global badge styles */
|
||||
header .bi-bell .badge {
|
||||
width: 22px !important;
|
||||
height: 22px !important;
|
||||
min-width: 22px !important;
|
||||
padding: 0 !important;
|
||||
font-size: 12px !important;
|
||||
line-height: 22px !important;
|
||||
border-radius: 50% !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: none !important;
|
||||
box-shadow: 0 0 0 2px #ffffff;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="sidebar">
|
||||
@@ -235,13 +264,24 @@
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Shipments --}}
|
||||
<!-- {{-- Shipments --}}
|
||||
@can('shipment.view')
|
||||
<a href="{{ route('admin.shipments') }}" class="{{ request()->routeIs('admin.shipments') ? 'active' : '' }}">
|
||||
<i class="bi bi-truck"></i> Shipments
|
||||
</a>
|
||||
@endcan
|
||||
@endcan -->
|
||||
|
||||
|
||||
|
||||
{{-- Container NEW MENU --}}
|
||||
@can('container.view')
|
||||
<a href="{{ route('containers.index') }}" class="{{ request()->routeIs('containers.*') ? 'active' : '' }}">
|
||||
<i class="fa-solid fa-box"></i> Container
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
|
||||
|
||||
{{-- Invoice --}}
|
||||
@can('invoice.view')
|
||||
<a href="{{ route('admin.invoices.index') }}" class="{{ request()->routeIs('admin.invoices.index') ? 'active' : '' }}">
|
||||
@@ -282,12 +322,12 @@
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Profile Update Requests --}}
|
||||
<!-- {{-- Profile Update Requests --}}
|
||||
@can('request.update_profile')
|
||||
<a href="{{ route('admin.profile.requests') }}">
|
||||
<i class="bi bi-person-lines-fill"></i> Profile Update Requests
|
||||
</a>
|
||||
@endcan
|
||||
@endcan -->
|
||||
|
||||
{{-- Staff (NO PERMISSION REQUIRED) --}}
|
||||
<a href="{{ route('admin.staff.index') }}" class="{{ request()->routeIs('admin.staff.*') ? 'active' : '' }}">
|
||||
@@ -346,6 +386,8 @@
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
@vite(['resources/js/app.js'])
|
||||
@yield('scripts') <!-- ⭐ REQUIRED FOR CHAT TO WORK -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const headerToggle = document.getElementById('headerToggle');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,60 +11,64 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h3>Orders Report</h3>
|
||||
|
||||
@if(!empty($filters))
|
||||
<p>
|
||||
@if($filters['search']) Search: <strong>{{ $filters['search'] }}</strong> @endif
|
||||
@if($filters['status']) | Status: <strong>{{ ucfirst($filters['status']) }}</strong> @endif
|
||||
@if($filters['shipment']) | Shipment: <strong>{{ ucfirst($filters['shipment']) }}</strong> @endif
|
||||
</p>
|
||||
@endif
|
||||
<h3>Orders Report</h3>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
@if(!empty($filters))
|
||||
<p>
|
||||
@if(!empty($filters['search'])) Search: <strong>{{ $filters['search'] }}</strong> @endif
|
||||
@if(!empty($filters['status'])) | Status: <strong>{{ ucfirst($filters['status']) }}</strong> @endif
|
||||
@if(!empty($filters['shipment'])) | Shipment: <strong>{{ ucfirst($filters['shipment']) }}</strong> @endif
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order ID</th>
|
||||
<th>Shipment ID</th>
|
||||
<th>Customer ID</th>
|
||||
<th>Company</th>
|
||||
<th>Origin</th>
|
||||
<th>Destination</th>
|
||||
<th>Order Date</th>
|
||||
<th>Invoice No</th>
|
||||
<th>Invoice Date</th>
|
||||
<th>Amount</th>
|
||||
<th>Amount + GST</th>
|
||||
<th>Invoice Status</th>
|
||||
<th>Shipment Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($orders as $order)
|
||||
@php
|
||||
$mark = $order->markList;
|
||||
$invoice = $order->invoice;
|
||||
$shipment = $order->shipments->first();
|
||||
@endphp
|
||||
<tr>
|
||||
<th>Order ID</th>
|
||||
<th>Shipment ID</th>
|
||||
<th>Customer ID</th>
|
||||
<th>Company</th>
|
||||
<th>Origin</th>
|
||||
<th>Destination</th>
|
||||
<th>Order Date</th>
|
||||
<th>Invoice No</th>
|
||||
<th>Invoice Date</th>
|
||||
<th>Amount</th>
|
||||
<th>Amount + GST</th>
|
||||
<th>Invoice Status</th>
|
||||
<th>Shipment Status</th>
|
||||
<td>{{ $order->order_id }}</td>
|
||||
<td>{{ $shipment?->shipment_id ?? '-' }}</td>
|
||||
<td>{{ $mark?->customer_id ?? '-' }}</td>
|
||||
<td>{{ $mark?->company_name ?? '-' }}</td>
|
||||
<td>{{ $mark?->origin ?? $order->origin ?? '-' }}</td>
|
||||
<td>{{ $mark?->destination ?? $order->destination ?? '-' }}</td>
|
||||
<td>{{ $order->created_at?->format('d-m-Y') ?? '-' }}</td>
|
||||
<td>{{ $invoice?->invoice_number ?? '-' }}</td>
|
||||
<td>{{ $invoice?->invoice_date ? \Carbon\Carbon::parse($invoice->invoice_date)->format('d-m-Y') : '-' }}</td>
|
||||
<td>{{ $invoice?->final_amount !== null ? number_format($invoice->final_amount, 2) : '-' }}</td>
|
||||
<td>{{ $invoice?->final_amount_with_gst !== null ? number_format($invoice->final_amount_with_gst, 2) : '-' }}</td>
|
||||
<td>{{ ucfirst($invoice?->status ?? 'Pending') }}</td>
|
||||
<td>{{ ucfirst(str_replace('_',' ', $shipment?->status ?? 'Pending')) }}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($orders as $order)
|
||||
@php
|
||||
$mark = $order->markList ?? null;
|
||||
$invoice = $order->invoice ?? null;
|
||||
$shipment = $order->shipments->first() ?? null;
|
||||
@endphp
|
||||
<tr>
|
||||
<td>{{ $order->order_id }}</td>
|
||||
<td>{{ $shipment->shipment_id ?? '-' }}</td>
|
||||
<td>{{ $mark->customer_id ?? '-' }}</td>
|
||||
<td>{{ $mark->company_name ?? '-' }}</td>
|
||||
<td>{{ $mark->origin ?? $order->origin ?? '-' }}</td>
|
||||
<td>{{ $mark->destination ?? $order->destination ?? '-' }}</td>
|
||||
<td>{{ $order->created_at ? $order->created_at->format('d-m-Y') : '-' }}</td>
|
||||
<td>{{ $invoice->invoice_number ?? '-' }}</td>
|
||||
<td>{{ $invoice?->invoice_date ? \Carbon\Carbon::parse($invoice->invoice_date)->format('d-m-Y') : '-' }}</td>
|
||||
<td>{{ $invoice?->final_amount ? number_format($invoice->final_amount, 2) : '-' }}</td>
|
||||
<td>{{ $invoice?->final_amount_with_gst ? number_format($invoice->final_amount_with_gst, 2) : '-' }}</td>
|
||||
<td>{{ $invoice->status ? ucfirst($invoice->status) : 'Pending' }}</td>
|
||||
<td>{{ $shipment?->status ? ucfirst(str_replace('_',' ',$shipment->status)) : 'Pending' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="13" style="text-align:center">No orders found</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="13" style="text-align:center">No orders found</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -13,47 +13,54 @@
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h4 class="fw-bold mb-0">Order Details</h4>
|
||||
@php
|
||||
$status = strtolower($order->status ?? '');
|
||||
@endphp
|
||||
|
||||
<small class="text-muted">Detailed view of this shipment order</small>
|
||||
</div>
|
||||
|
||||
{{-- ADD ITEM --}}
|
||||
@can('order.create')
|
||||
<button class="btn btn-add-item" data-bs-toggle="modal" data-bs-target="#addItemModal">
|
||||
<i class="fas fa-plus-circle me-2"></i>Add New Item
|
||||
</button>
|
||||
@if($status === 'pending')
|
||||
<button class="btn btn-add-item" data-bs-toggle="modal" data-bs-target="#addItemModal">
|
||||
<i class="fas fa-plus-circle me-2"></i>Add New Item
|
||||
</button>
|
||||
@endif
|
||||
@endcan
|
||||
|
||||
|
||||
<a href="{{ route('admin.dashboard') }}" class="btn-close"></a>
|
||||
</div>
|
||||
|
||||
<!-- {{-- ACTION BUTTONS --}}
|
||||
<div class="mt-3 d-flex gap-2">
|
||||
<div class="mt-3 d-flex gap-2">-->
|
||||
|
||||
|
||||
<div class="mt-3 d-flex gap-2">
|
||||
{{-- Edit Order --}}
|
||||
@if($status === 'pending')
|
||||
<button class="btn btn-edit-order"
|
||||
onclick="document.getElementById('editOrderForm').style.display='block'">
|
||||
<i class="fas fa-edit me-2"></i>Edit Order
|
||||
</button>
|
||||
@endif
|
||||
|
||||
{{-- EDIT ORDER --}} -->
|
||||
<!-- @if($order->status === 'pending')
|
||||
<button class="btn btn-edit-order" onclick="document.getElementById('editOrderForm').style.display='block'">
|
||||
<i class="fas fa-edit me-2"></i>Edit Order
|
||||
</button>
|
||||
@else
|
||||
<button class="btn btn-edit-order" disabled><i class="fas fa-edit me-2"></i>Edit Order</button>
|
||||
@endif -->
|
||||
{{-- Delete Order --}}
|
||||
@if($status === 'pending')
|
||||
<form action="{{ route('admin.orders.destroy', $order->id) }}"
|
||||
method="POST"
|
||||
onsubmit="return confirm('Delete this entire order?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button class="btn btn-delete-order">
|
||||
<i class="fas fa-trash-alt me-2"></i>Delete Order
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- {{-- DELETE ORDER --}}
|
||||
@if($order->status === 'pending')
|
||||
<form action="{{ route('admin.orders.destroy', $order->id) }}"
|
||||
method="POST"
|
||||
onsubmit="return confirm('Delete this entire order?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button class="btn btn-delete-order">
|
||||
<i class="fas fa-trash-alt me-2"></i>Delete Order
|
||||
</button>
|
||||
</form>
|
||||
@endif -->
|
||||
|
||||
<!-- </div> -->
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -190,32 +197,33 @@
|
||||
<td>{{ $item->ttl_kg }}</td>
|
||||
<td>{{ $item->shop_no }}</td>
|
||||
|
||||
<td class="d-flex justify-content-center gap-2">
|
||||
<td class="d-flex justify-content-center gap-2">
|
||||
@if($status === 'pending')
|
||||
{{-- EDIT BUTTON --}}
|
||||
@can('order.edit')
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-edit-item"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#editItemModal{{ $item->id }}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
@endcan
|
||||
|
||||
{{-- EDIT BUTTON --}}
|
||||
@can('order.edit')
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-edit-item"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#editItemModal{{ $item->id }}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
@endcan
|
||||
{{-- DELETE BUTTON --}}
|
||||
@can('order.delete')
|
||||
<form action="{{ route('admin.orders.deleteItem', $item->id) }}"
|
||||
method="POST"
|
||||
onsubmit="return confirm('Delete this item?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-delete-item">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endcan
|
||||
@endif
|
||||
</td>
|
||||
|
||||
@can('order.delete')
|
||||
{{-- DELETE BUTTON --}}
|
||||
<form action="{{ route('admin.orders.deleteItem', $item->id) }}"
|
||||
method="POST"
|
||||
onsubmit="return confirm('Delete this item?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button class="btn btn-sm btn-delete-item">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endcan
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
@endforeach
|
||||
@@ -617,7 +625,7 @@ function fillFormFromDeleted(item) {
|
||||
box-shadow: 0 4px 15px 0 rgba(102, 126, 234, 0.3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-right: -800px;
|
||||
margin-right: -650px;
|
||||
}
|
||||
|
||||
.btn-add-item:hover {
|
||||
|
||||
32
resources/views/admin/pdf/invoices_excel.blade.php
Normal file
32
resources/views/admin/pdf/invoices_excel.blade.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Invoice No</th>
|
||||
<th>Invoice Date</th>
|
||||
<th>Mark No</th>
|
||||
<th>Container No</th>
|
||||
<th>Container Date</th>
|
||||
<th>Company</th>
|
||||
<th>Customer</th>
|
||||
<th>Amount</th>
|
||||
<th>Amount + GST</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($invoices as $inv)
|
||||
<tr>
|
||||
<td>{{ $inv->invoice_number }}</td>
|
||||
<td>{{ $inv->invoice_date ? \Carbon\Carbon::parse($inv->invoice_date)->format('d-m-Y') : '' }}</td>
|
||||
<td>{{ $inv->mark_no }}</td>
|
||||
<td>{{ $inv->container_number }}</td>
|
||||
<td>{{ $inv->container_date ? \Carbon\Carbon::parse($inv->container_date)->format('d-m-Y') : '' }}</td>
|
||||
<td>{{ $inv->company_name }}</td>
|
||||
<td>{{ $inv->customer_name }}</td>
|
||||
<td>{{ $inv->final_amount }}</td>
|
||||
<td>{{ $inv->final_amount_with_gst }}</td>
|
||||
<td>{{ $inv->invoice_status }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
48
resources/views/admin/pdf/invoices_report.blade.php
Normal file
48
resources/views/admin/pdf/invoices_report.blade.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Invoices Report</title>
|
||||
<style>
|
||||
body { font-family: DejaVu Sans, sans-serif; font-size: 12px; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||
th, td { border: 1px solid #ccc; padding: 6px 8px; text-align: left; }
|
||||
th { background: #f3f4f6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h3>Invoices Report</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Invoice No</th>
|
||||
<th>Invoice Date</th>
|
||||
<th>Mark No</th>
|
||||
<th>Container No</th>
|
||||
<th>Container Date</th>
|
||||
<th>Company</th>
|
||||
<th>Customer</th>
|
||||
<th>Amount</th>
|
||||
<th>Amount + GST</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($invoices as $inv)
|
||||
<tr>
|
||||
<td>{{ $inv->invoice_number ?? '-' }}</td>
|
||||
<td>{{ $inv->invoice_date ? \Carbon\Carbon::parse($inv->invoice_date)->format('d-m-Y') : '-' }}</td>
|
||||
<td>{{ $inv->mark_no ?? '-' }}</td>
|
||||
<td>{{ $inv->container_number ?? '-' }}</td>
|
||||
<td>{{ $inv->container_date ? \Carbon\Carbon::parse($inv->container_date)->format('d-m-Y') : '-' }}</td>
|
||||
<td>{{ $inv->company_name ?? '-' }}</td>
|
||||
<td>{{ $inv->customer_name ?? '-' }}</td>
|
||||
<td>{{ $inv->final_amount ? number_format($inv->final_amount, 2) : '0.00' }}</td>
|
||||
<td>{{ $inv->final_amount_with_gst ? number_format($inv->final_amount_with_gst, 2) : '0.00' }}</td>
|
||||
<td>{{ ucfirst($inv->invoice_status ?? 'pending') }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user