Compare commits
52 Commits
main
...
33571a5fd7
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
33571a5fd7 | ||
|
|
94e211f87e | ||
|
|
8b6d3d5fad | ||
|
|
c89e5bdf7d | ||
|
|
10af713fa1 | ||
|
|
ebb263cd36 | ||
|
|
82d9c10130 | ||
|
|
cb24cf575b | ||
|
|
e4c07cb838 | ||
|
|
f38a5afdd7 | ||
|
|
9423c79c80 | ||
|
|
8a958b9c48 | ||
|
|
338425535e | ||
|
|
f7856a6755 | ||
|
|
8f95091673 | ||
|
|
7362ef6bdc | ||
|
|
e872b83ea3 | ||
|
|
82882e859e | ||
|
|
2d28e7c1d5 | ||
|
|
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_DEPRECATIONS_CHANNEL=null
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
DB_CONNECTION=sqlite
|
DB_CONNECTION=mysql
|
||||||
# DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
# DB_PORT=3306
|
DB_PORT=3306
|
||||||
# DB_DATABASE=laravel
|
DB_DATABASE=kent_logistics6
|
||||||
# DB_USERNAME=root
|
DB_USERNAME=root
|
||||||
# DB_PASSWORD=
|
DB_PASSWORD=
|
||||||
|
|
||||||
SESSION_DRIVER=database
|
SESSION_DRIVER=database
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
|
|||||||
87
app/Events/NewChatMessage.php
Normal file
87
app/Events/NewChatMessage.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Models\ChatMessage;
|
||||||
|
use Illuminate\Broadcasting\PrivateChannel;
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class NewChatMessage implements ShouldBroadcast
|
||||||
|
{
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
public $message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*/
|
||||||
|
public function __construct(ChatMessage $message)
|
||||||
|
{
|
||||||
|
// Safe data only (no heavy relationships in queue)
|
||||||
|
$this->message = [
|
||||||
|
'id' => $message->id,
|
||||||
|
'ticket_id' => $message->ticket_id,
|
||||||
|
'sender_id' => $message->sender_id,
|
||||||
|
'sender_type' => $message->sender_type,
|
||||||
|
'message' => $message->message,
|
||||||
|
'file_path' => $message->file_path,
|
||||||
|
'file_type' => $message->file_type,
|
||||||
|
'created_at' => $message->created_at->toDateTimeString(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Load sender separately for broadcastWith()
|
||||||
|
$this->sender = $message->sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The channel the event should broadcast on.
|
||||||
|
*/
|
||||||
|
public function broadcastOn()
|
||||||
|
{
|
||||||
|
return new PrivateChannel('ticket.' . $this->message->ticket_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data sent to frontend (Blade + Flutter)
|
||||||
|
*/
|
||||||
|
public function broadcastWith()
|
||||||
|
{
|
||||||
|
\Log::info("DEBUG: NewChatMessage broadcasting on channel ticket.".$this->message->ticket_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,
|
||||||
|
'file_url' => $this->message->file_path
|
||||||
|
? asset('storage/' . $this->message->file_path)
|
||||||
|
: null,
|
||||||
|
'file_type' => $this->message->file_type,
|
||||||
|
'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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,10 +6,11 @@ use App\Models\Order;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
class OrdersExport implements FromCollection, WithHeadings
|
class OrdersExport implements FromCollection, WithHeadings
|
||||||
{
|
{
|
||||||
protected $request;
|
protected Request $request;
|
||||||
|
|
||||||
public function __construct(Request $request)
|
public function __construct(Request $request)
|
||||||
{
|
{
|
||||||
@@ -18,61 +19,99 @@ class OrdersExport implements FromCollection, WithHeadings
|
|||||||
|
|
||||||
private function buildQuery()
|
private function buildQuery()
|
||||||
{
|
{
|
||||||
$query = Order::with(['markList', 'invoice', 'shipments']);
|
$query = Order::query()->with([
|
||||||
|
'markList',
|
||||||
|
'invoice',
|
||||||
|
'shipments',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// SEARCH
|
||||||
if ($this->request->filled('search')) {
|
if ($this->request->filled('search')) {
|
||||||
$search = $this->request->search;
|
$search = trim($this->request->search);
|
||||||
$query->where(function($q) use ($search) {
|
|
||||||
$q->where('order_id', 'like', "%{$search}%")
|
$query->where(function ($q) use ($search) {
|
||||||
->orWhereHas('markList', function($q2) use ($search) {
|
$q->where('orders.order_id', 'like', "%{$search}%")
|
||||||
|
->orWhereHas('markList', function ($q2) use ($search) {
|
||||||
$q2->where('company_name', 'like', "%{$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}%");
|
$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')) {
|
if ($this->request->filled('status')) {
|
||||||
$query->whereHas('invoice', function($q) {
|
$query->where(function ($q) {
|
||||||
$q->where('status', $this->request->status);
|
$q->whereHas('invoice', function ($q2) {
|
||||||
|
$q2->where('status', $this->request->status);
|
||||||
|
})
|
||||||
|
->orWhereDoesntHave('invoice');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// SHIPMENT STATUS (FIXED)
|
||||||
if ($this->request->filled('shipment')) {
|
if ($this->request->filled('shipment')) {
|
||||||
$query->whereHas('shipments', function($q) {
|
$query->where(function ($q) {
|
||||||
$q->where('status', $this->request->shipment);
|
$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()
|
public function collection()
|
||||||
{
|
{
|
||||||
$orders = $this->buildQuery()->get();
|
return $this->buildQuery()->get()->map(function ($order) {
|
||||||
|
|
||||||
// Map to simple array rows suitable for Excel
|
$mark = $order->markList;
|
||||||
return $orders->map(function($order) {
|
$invoice = $order->invoice;
|
||||||
$mark = $order->markList;
|
$shipment = $order->shipments->first();
|
||||||
$invoice = $order->invoice;
|
|
||||||
$shipment = $order->shipments->first() ?? null;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'Order ID' => $order->order_id,
|
'Order ID' => $order->order_id ?? '-',
|
||||||
'Shipment ID' => $shipment->shipment_id ?? '-',
|
'Shipment ID' => $shipment?->shipment_id ?? '-',
|
||||||
'Customer ID' => $mark->customer_id ?? '-',
|
'Customer ID' => $mark?->customer_id ?? '-',
|
||||||
'Company' => $mark->company_name ?? '-',
|
'Company' => $mark?->company_name ?? '-',
|
||||||
'Origin' => $mark->origin ?? $order->origin ?? '-',
|
'Origin' => $mark?->origin ?? $order->origin ?? '-',
|
||||||
'Destination' => $mark->destination ?? $order->destination ?? '-',
|
'Destination' => $mark?->destination ?? $order->destination ?? '-',
|
||||||
'Order Date' => $order->created_at ? $order->created_at->format('d-m-Y') : '-',
|
'Order Date' => $order->created_at
|
||||||
'Invoice No' => $invoice->invoice_number ?? '-',
|
? $order->created_at->format('d-m-Y')
|
||||||
'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) : '-',
|
'Invoice No' => $invoice?->invoice_number ?? '-',
|
||||||
'Amount + GST' => $invoice?->final_amount_with_gst ? number_format($invoice->final_amount_with_gst, 2) : '-',
|
'Invoice Date' => $invoice?->invoice_date
|
||||||
'Invoice Status' => $invoice->status ? ucfirst($invoice->status) : 'Pending',
|
? Carbon::parse($invoice->invoice_date)->format('d-m-Y')
|
||||||
'Shipment Status' => $shipment?->status ? ucfirst(str_replace('_', ' ', $shipment->status)) : 'Pending',
|
: '-',
|
||||||
|
'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')),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
155
app/Http/Controllers/Admin/AdminChatController.php
Normal file
155
app/Http/Controllers/Admin/AdminChatController.php
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
<<<<<<< HEAD
|
||||||
|
use App\Models\SupportTicket;
|
||||||
|
use App\Models\ChatMessage;
|
||||||
|
use App\Events\NewChatMessage;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
=======
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\SupportTicket;
|
||||||
|
use App\Models\ChatMessage;
|
||||||
|
use App\Events\NewChatMessage;
|
||||||
|
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||||
|
|
||||||
|
class AdminChatController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
<<<<<<< HEAD
|
||||||
|
* Page 1: List all customer chat tickets
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$tickets = SupportTicket::with(['user', 'messages' => function($query) {
|
||||||
|
$query->latest()->limit(1);
|
||||||
|
}])
|
||||||
|
->orderBy('updated_at', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('admin.chat_support', compact('tickets'));
|
||||||
|
}
|
||||||
|
=======
|
||||||
|
* 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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page 2: Open chat window for a specific user
|
||||||
|
*/
|
||||||
|
public function openChat($ticketId)
|
||||||
|
<<<<<<< HEAD
|
||||||
|
{
|
||||||
|
$ticket = SupportTicket::with('user')->findOrFail($ticketId);
|
||||||
|
$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 (FIXED - LIVE CHAT)
|
||||||
|
=======
|
||||||
|
{
|
||||||
|
$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
|
||||||
|
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
|
||||||
|
'read_by_admin' => true,
|
||||||
|
'read_by_user' => false,
|
||||||
|
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
\Log::info("DEBUG: ChatController sendMessage called", [
|
||||||
|
=======
|
||||||
|
\Log::info("DEBUG: ChatController sendMessage called", [
|
||||||
|
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||||
|
'ticket_id' => $ticketId,
|
||||||
|
'payload' => $request->all()
|
||||||
|
]);
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
// 🔥 LIVE CHAT - Queue bypass (100% working)
|
||||||
|
broadcast(new NewChatMessage($message))->toOthers();
|
||||||
|
|
||||||
|
=======
|
||||||
|
// Broadcast real-time
|
||||||
|
broadcast(new NewChatMessage($message));
|
||||||
|
|
||||||
|
\Log::info("DEBUG: ChatController sendMessage called 79", [
|
||||||
|
'ticket_id' => $ticketId,
|
||||||
|
'payload' => $request->all()
|
||||||
|
]);
|
||||||
|
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => $message
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,32 +19,37 @@ class AdminCustomerController extends Controller
|
|||||||
$search = $request->search;
|
$search = $request->search;
|
||||||
$status = $request->status;
|
$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)) {
|
if (!empty($search)) {
|
||||||
$query->where(function ($q) use ($search) {
|
$query->where(function ($q) use ($search) {
|
||||||
$q->where('customer_name', 'like', "%$search%")
|
$q->where('customer_name', 'like', "%$search%")
|
||||||
->orWhere('email', 'like', "%$search%")
|
->orWhere('email', 'like', "%$search%")
|
||||||
->orWhere('mobile_no', 'like', "%$search%")
|
->orWhere('mobile_no', 'like', "%$search%")
|
||||||
->orWhere('customer_id', 'like', "%$search%");
|
->orWhere('customer_id', 'like', "%$search%");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// STATUS FILTER
|
|
||||||
if (!empty($status) && in_array($status, ['active', 'inactive'])) {
|
if (!empty($status) && in_array($status, ['active', 'inactive'])) {
|
||||||
$query->where('status', $status);
|
$query->where('status', $status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all customers for statistics (without pagination)
|
|
||||||
$allCustomers = $query->get();
|
$allCustomers = $query->get();
|
||||||
|
|
||||||
// Get paginated customers for the table (10 per page)
|
|
||||||
$customers = $query->paginate(10);
|
$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
|
// SHOW ADD CUSTOMER FORM
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
@@ -106,20 +111,36 @@ class AdminCustomerController extends Controller
|
|||||||
// VIEW CUSTOMER FULL DETAILS
|
// VIEW CUSTOMER FULL DETAILS
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
public function view($id)
|
public function view($id)
|
||||||
{
|
{
|
||||||
$customer = User::with(['marks', 'orders'])->findOrFail($id);
|
$customer = User::with([
|
||||||
|
'marks',
|
||||||
|
'orders',
|
||||||
|
'invoices.installments'
|
||||||
|
])->findOrFail($id);
|
||||||
|
|
||||||
$totalOrders = $customer->orders->count();
|
// Orders
|
||||||
$totalAmount = $customer->orders->sum('ttl_amount');
|
$totalOrders = $customer->orders->count();
|
||||||
$recentOrders = $customer->orders()->latest()->take(5)->get();
|
$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
|
// TOGGLE STATUS ACTIVE / INACTIVE
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
use App\Models\InvoiceItem;
|
use App\Models\InvoiceItem;
|
||||||
use Mpdf\Mpdf;
|
|
||||||
use App\Models\InvoiceInstallment;
|
use App\Models\InvoiceInstallment;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Mpdf\Mpdf;
|
||||||
|
|
||||||
class AdminInvoiceController extends Controller
|
class AdminInvoiceController extends Controller
|
||||||
{
|
{
|
||||||
@@ -17,7 +17,10 @@ class AdminInvoiceController extends Controller
|
|||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$invoices = Invoice::with(['order.shipments'])->latest()->get();
|
$invoices = Invoice::with(['items', 'customer', 'container'])
|
||||||
|
->latest()
|
||||||
|
->get();
|
||||||
|
|
||||||
return view('admin.invoice', compact('invoices'));
|
return view('admin.invoice', compact('invoices'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,15 +29,10 @@ class AdminInvoiceController extends Controller
|
|||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
public function popup($id)
|
public function popup($id)
|
||||||
{
|
{
|
||||||
$invoice = Invoice::with(['items', 'order'])->findOrFail($id);
|
$invoice = Invoice::with(['items', 'customer', 'container'])->findOrFail($id);
|
||||||
|
$shipment = null;
|
||||||
|
|
||||||
// Find actual Shipment record
|
return view('admin.popup_invoice', compact('invoice', 'shipment'));
|
||||||
$shipment = \App\Models\Shipment::whereHas('items', function ($q) use ($invoice) {
|
|
||||||
$q->where('order_id', $invoice->order_id);
|
|
||||||
})
|
|
||||||
->first();
|
|
||||||
|
|
||||||
return view('admin.popup_invoice', compact('invoice', 'shipment'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
@@ -42,111 +40,236 @@ class AdminInvoiceController extends Controller
|
|||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
public function edit($id)
|
public function edit($id)
|
||||||
{
|
{
|
||||||
$invoice = Invoice::with(['order.shipments'])->findOrFail($id);
|
$invoice = Invoice::with(['items', 'customer', 'container'])->findOrFail($id);
|
||||||
$shipment = $invoice->order?->shipments?->first();
|
$shipment = null;
|
||||||
|
|
||||||
return view('admin.invoice_edit', compact('invoice', 'shipment'));
|
// ADD THIS SECTION: Calculate customer's total due across all invoices
|
||||||
|
$customerTotalDue = Invoice::where('customer_id', $invoice->customer_id)
|
||||||
|
->where('status', '!=', 'cancelled')
|
||||||
|
->where('status', '!=', 'void')
|
||||||
|
->sum('final_amount_with_gst');
|
||||||
|
|
||||||
|
// Pass the new variable to the view
|
||||||
|
return view('admin.invoice_edit', compact('invoice', 'shipment', 'customerTotalDue'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// UPDATE INVOICE
|
// UPDATE INVOICE (HEADER LEVEL)
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// UPDATE INVOICE (HEADER LEVEL)
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
public function update(Request $request, $id)
|
public function update(Request $request, $id)
|
||||||
{
|
{
|
||||||
Log::info("🟡 Invoice Update Request Received", [
|
Log::info('🟡 Invoice Update Request Received', [
|
||||||
'invoice_id' => $id,
|
'invoice_id' => $id,
|
||||||
'request' => $request->all()
|
'request' => $request->all(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$invoice = Invoice::findOrFail($id);
|
$invoice = Invoice::findOrFail($id);
|
||||||
|
|
||||||
|
// 1) VALIDATION
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'invoice_date' => 'required|date',
|
'invoice_date' => 'required|date',
|
||||||
'due_date' => 'required|date|after_or_equal:invoice_date',
|
'due_date' => 'required|date|after_or_equal:invoice_date',
|
||||||
'final_amount' => 'required|numeric|min:0',
|
'final_amount' => 'required|numeric|min:0',
|
||||||
'tax_type' => 'required|in:gst,igst',
|
'tax_type' => 'required|in:gst,igst',
|
||||||
'tax_percent' => 'required|numeric|min:0|max:28',
|
'tax_percent' => 'required|numeric|min:0|max:28',
|
||||||
'status' => 'required|in:pending,paid,overdue',
|
'status' => 'required|in:pending,paid,overdue',
|
||||||
'notes' => 'nullable|string',
|
'notes' => 'nullable|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Log::info("✅ Validated Invoice Update Data", $data);
|
Log::info('✅ Validated Invoice Update Data', $data);
|
||||||
|
|
||||||
$finalAmount = floatval($data['final_amount']);
|
// 2) CALCULATE GST / TOTALS
|
||||||
$taxPercent = floatval($data['tax_percent']);
|
$finalAmount = (float) $data['final_amount'];
|
||||||
$taxAmount = 0;
|
$taxPercent = (float) $data['tax_percent'];
|
||||||
|
|
||||||
if ($data['tax_type'] === 'gst') {
|
if ($data['tax_type'] === 'gst') {
|
||||||
Log::info("🟢 GST Selected", compact('taxPercent'));
|
Log::info('🟢 GST Selected', compact('taxPercent'));
|
||||||
|
|
||||||
$data['cgst_percent'] = $taxPercent / 2;
|
$data['cgst_percent'] = $taxPercent / 2;
|
||||||
$data['sgst_percent'] = $taxPercent / 2;
|
$data['sgst_percent'] = $taxPercent / 2;
|
||||||
$data['igst_percent'] = 0;
|
$data['igst_percent'] = 0;
|
||||||
} else {
|
} else {
|
||||||
Log::info("🔵 IGST Selected", compact('taxPercent'));
|
Log::info('🔵 IGST Selected', compact('taxPercent'));
|
||||||
|
|
||||||
$data['cgst_percent'] = 0;
|
$data['cgst_percent'] = 0;
|
||||||
$data['sgst_percent'] = 0;
|
$data['sgst_percent'] = 0;
|
||||||
$data['igst_percent'] = $taxPercent;
|
$data['igst_percent'] = $taxPercent;
|
||||||
}
|
}
|
||||||
|
|
||||||
$taxAmount = ($finalAmount * $taxPercent) / 100;
|
$gstAmount = ($finalAmount * $taxPercent) / 100;
|
||||||
|
$data['gst_amount'] = $gstAmount;
|
||||||
$data['gst_amount'] = $taxAmount;
|
$data['final_amount_with_gst'] = $finalAmount + $gstAmount;
|
||||||
$data['final_amount_with_gst'] = $finalAmount + $taxAmount;
|
$data['gst_percent'] = $taxPercent;
|
||||||
$data['gst_percent'] = $taxPercent;
|
|
||||||
|
Log::info('📌 Final Calculated Invoice Values', [
|
||||||
Log::info("📌 Final Calculated Invoice Values", [
|
'invoice_id' => $invoice->id,
|
||||||
'invoice_id' => $invoice->id,
|
'final_amount' => $finalAmount,
|
||||||
'final_amount' => $finalAmount,
|
'gst_amount' => $data['gst_amount'],
|
||||||
'gst_amount' => $data['gst_amount'],
|
'final_amount_with_gst' => $data['final_amount_with_gst'],
|
||||||
'final_amount_with_gst' => $data['final_amount_with_gst'],
|
'tax_type' => $data['tax_type'],
|
||||||
'tax_type' => $data['tax_type'],
|
'cgst_percent' => $data['cgst_percent'],
|
||||||
'cgst_percent' => $data['cgst_percent'],
|
'sgst_percent' => $data['sgst_percent'],
|
||||||
'sgst_percent' => $data['sgst_percent'],
|
'igst_percent' => $data['igst_percent'],
|
||||||
'igst_percent' => $data['igst_percent'],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 3) UPDATE DB
|
||||||
$invoice->update($data);
|
$invoice->update($data);
|
||||||
|
|
||||||
Log::info("✅ Invoice Updated Successfully", [
|
Log::info('✅ Invoice Updated Successfully', [
|
||||||
'invoice_id' => $invoice->id
|
'invoice_id' => $invoice->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// regenerate PDF
|
// 4) LOG ACTUAL DB VALUES
|
||||||
|
$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,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 5) REGENERATE PDF
|
||||||
$this->generateInvoicePDF($invoice);
|
$this->generateInvoicePDF($invoice);
|
||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('admin.invoices.index')
|
->route('admin.invoices.index')
|
||||||
->with('success', 'Invoice updated & PDF generated successfully.');
|
->with('success', 'Invoice updated & PDF generated successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// 🔹 UPDATE INVOICE ITEMS (price + ttl_amount)
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
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();
|
||||||
|
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', 'Invoice items updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// PDF GENERATION USING mPDF
|
// PDF GENERATION USING mPDF
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
public function generateInvoicePDF($invoice)
|
public function generateInvoicePDF($invoice)
|
||||||
{
|
{
|
||||||
$invoice->load(['items', 'order.shipments']);
|
$invoice->load(['items', 'customer', 'container']);
|
||||||
$shipment = $invoice->order?->shipments?->first();
|
$shipment = null;
|
||||||
|
|
||||||
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
|
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
|
||||||
$folder = public_path('invoices/');
|
$folder = public_path('invoices/');
|
||||||
|
|
||||||
if (!file_exists($folder)) {
|
if (!file_exists($folder)) {
|
||||||
mkdir($folder, 0777, true);
|
mkdir($folder, 0777, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
$filePath = $folder . $fileName;
|
$filePath = $folder . $fileName;
|
||||||
|
|
||||||
if (file_exists($filePath)) {
|
if (file_exists($filePath)) {
|
||||||
unlink($filePath);
|
unlink($filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
$mpdf = new Mpdf(['mode' => 'utf-8', 'format' => 'A4', 'default_font' => 'sans-serif']);
|
$mpdf = new Mpdf([
|
||||||
$html = view('admin.pdf.invoice', ['invoice' => $invoice, 'shipment' => $shipment])->render();
|
'mode' => 'utf-8',
|
||||||
|
'format' => 'A4',
|
||||||
|
'default_font' => 'sans-serif',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$html = view('admin.pdf.invoice', [
|
||||||
|
'invoice' => $invoice,
|
||||||
|
'shipment' => $shipment,
|
||||||
|
])->render();
|
||||||
|
|
||||||
$mpdf->WriteHTML($html);
|
$mpdf->WriteHTML($html);
|
||||||
$mpdf->Output($filePath, 'F');
|
$mpdf->Output($filePath, 'F');
|
||||||
|
|
||||||
$invoice->update(['pdf_path' => 'invoices/' . $fileName]);
|
$invoice->update(['pdf_path' => 'invoices/' . $fileName]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function downloadInvoice($id)
|
||||||
|
{
|
||||||
|
$invoice = Invoice::findOrFail($id);
|
||||||
|
|
||||||
|
// ALWAYS regenerate to reflect latest HTML/CSS
|
||||||
|
$this->generateInvoicePDF($invoice);
|
||||||
|
$invoice->refresh();
|
||||||
|
|
||||||
|
return response()->download(public_path($invoice->pdf_path));
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// INSTALLMENTS (ADD/DELETE)
|
// INSTALLMENTS (ADD)
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
public function storeInstallment(Request $request, $invoice_id)
|
public function storeInstallment(Request $request, $invoice_id)
|
||||||
{
|
{
|
||||||
@@ -157,16 +280,14 @@ class AdminInvoiceController extends Controller
|
|||||||
'amount' => 'required|numeric|min:1',
|
'amount' => 'required|numeric|min:1',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$invoice = Invoice::findOrFail($invoice_id);
|
$invoice = Invoice::findOrFail($invoice_id);
|
||||||
|
|
||||||
$paidTotal = $invoice->installments()->sum('amount');
|
$paidTotal = $invoice->installments()->sum('amount');
|
||||||
// Use GST-inclusive total for all calculations/checks
|
|
||||||
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
||||||
|
|
||||||
if ($request->amount > $remaining) {
|
if ($request->amount > $remaining) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Installment amount exceeds remaining balance.'
|
'message' => 'Installment amount exceeds remaining balance.',
|
||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,48 +301,53 @@ class AdminInvoiceController extends Controller
|
|||||||
|
|
||||||
$newPaid = $paidTotal + $request->amount;
|
$newPaid = $paidTotal + $request->amount;
|
||||||
|
|
||||||
// Mark as 'paid' if GST-inclusive total is cleared
|
|
||||||
if ($newPaid >= $invoice->final_amount_with_gst) {
|
if ($newPaid >= $invoice->final_amount_with_gst) {
|
||||||
$invoice->update(['status' => 'paid']);
|
$invoice->update(['status' => 'paid']);
|
||||||
|
|
||||||
|
$this->generateInvoicePDF($invoice);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'Installment added successfully.',
|
'message' => 'Installment added successfully.',
|
||||||
'installment' => $installment,
|
'installment' => $installment,
|
||||||
'totalPaid' => $newPaid,
|
'totalPaid' => $newPaid,
|
||||||
'gstAmount' => $invoice->gst_amount,
|
'gstAmount' => $invoice->gst_amount,
|
||||||
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
||||||
'baseAmount' => $invoice->final_amount,
|
'baseAmount' => $invoice->final_amount,
|
||||||
'remaining' => max(0, $invoice->final_amount_with_gst - $newPaid),
|
'remaining' => max(0, $invoice->final_amount_with_gst - $newPaid),
|
||||||
'isCompleted' => $newPaid >= $invoice->final_amount_with_gst
|
'isCompleted' => $newPaid >= $invoice->final_amount_with_gst,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// INSTALLMENTS (DELETE)
|
||||||
|
// -------------------------------------------------------------
|
||||||
public function deleteInstallment($id)
|
public function deleteInstallment($id)
|
||||||
{
|
{
|
||||||
$installment = InvoiceInstallment::findOrFail($id);
|
$installment = InvoiceInstallment::findOrFail($id);
|
||||||
$invoice = $installment->invoice;
|
$invoice = $installment->invoice;
|
||||||
|
|
||||||
$installment->delete();
|
$installment->delete();
|
||||||
|
|
||||||
$paidTotal = $invoice->installments()->sum('amount');
|
$paidTotal = $invoice->installments()->sum('amount');
|
||||||
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
||||||
|
|
||||||
// Update status if not fully paid anymore
|
if ($remaining > 0 && $invoice->status === 'paid') {
|
||||||
if ($remaining > 0 && $invoice->status === "paid") {
|
|
||||||
$invoice->update(['status' => 'pending']);
|
$invoice->update(['status' => 'pending']);
|
||||||
|
|
||||||
|
$this->generateInvoicePDF($invoice);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'Installment deleted.',
|
'message' => 'Installment deleted.',
|
||||||
'totalPaid' => $paidTotal,
|
'totalPaid' => $paidTotal,
|
||||||
'gstAmount' => $invoice->gst_amount,
|
'gstAmount' => $invoice->gst_amount,
|
||||||
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
||||||
'baseAmount' => $invoice->final_amount,
|
'baseAmount' => $invoice->final_amount,
|
||||||
'remaining' => $remaining,
|
'remaining' => $remaining,
|
||||||
'isZero' => $paidTotal == 0
|
'isZero' => $paidTotal == 0,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -55,6 +55,7 @@ class AdminStaffController extends Controller
|
|||||||
DB::beginTransaction();
|
DB::beginTransaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 1️⃣ Create staff WITHOUT employee_id (ID not available yet)
|
||||||
$admin = Admin::create([
|
$admin = Admin::create([
|
||||||
'name' => $request->name,
|
'name' => $request->name,
|
||||||
'email' => $request->email,
|
'email' => $request->email,
|
||||||
@@ -69,23 +70,33 @@ class AdminStaffController extends Controller
|
|||||||
'status' => $request->status,
|
'status' => $request->status,
|
||||||
'additional_info' => $request->additional_info,
|
'additional_info' => $request->additional_info,
|
||||||
|
|
||||||
'username' => $request->username,
|
// username may be NULL here
|
||||||
|
'username' => $request->username ?: null,
|
||||||
'password' => Hash::make($request->password),
|
'password' => Hash::make($request->password),
|
||||||
'type' => 'staff',
|
'type' => 'staff',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Generate EMPLOYEE ID using admin ID (safe)
|
// 2️⃣ Generate EMPLOYEE ID
|
||||||
$employeeId = 'EMP' . str_pad($admin->id, 4, '0', STR_PAD_LEFT);
|
$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) {
|
if ($request->permissions) {
|
||||||
$admin->givePermissionTo($request->permissions);
|
$admin->givePermissionTo($request->permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
DB::commit();
|
DB::commit();
|
||||||
|
|
||||||
return redirect()->route('admin.staff.index')
|
return redirect()
|
||||||
|
->route('admin.staff.index')
|
||||||
->with('success', 'Staff created successfully.');
|
->with('success', 'Staff created successfully.');
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
@@ -94,6 +105,7 @@ class AdminStaffController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function edit($id)
|
public function edit($id)
|
||||||
{
|
{
|
||||||
$staff = Admin::where('type', 'staff')->findOrFail($id);
|
$staff = Admin::where('type', 'staff')->findOrFail($id);
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ class ShipmentController extends Controller
|
|||||||
$usedOrderIds = ShipmentItem::pluck('order_id')->toArray();
|
$usedOrderIds = ShipmentItem::pluck('order_id')->toArray();
|
||||||
|
|
||||||
// 2) Load available orders (not used in any shipment)
|
// 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
|
// 3) Load all shipments for listing
|
||||||
$shipments = Shipment::latest()->get();
|
$shipments = Shipment::latest()->get();
|
||||||
@@ -65,6 +69,16 @@ class ShipmentController extends Controller
|
|||||||
// CALCULATE TOTALS
|
// CALCULATE TOTALS
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
$orders = Order::whereIn('id', $request->order_ids)->get();
|
$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_ctn = $orders->sum('ctn');
|
||||||
$total_qty = $orders->sum('qty');
|
$total_qty = $orders->sum('qty');
|
||||||
@@ -82,7 +96,7 @@ class ShipmentController extends Controller
|
|||||||
'shipment_id' => $newShipmentId,
|
'shipment_id' => $newShipmentId,
|
||||||
'origin' => $request->origin,
|
'origin' => $request->origin,
|
||||||
'destination' => $request->destination,
|
'destination' => $request->destination,
|
||||||
'status' => Shipment::STATUS_PENDING,
|
'status' => Shipment::STATUS_SHIPMENT_READY,
|
||||||
'shipment_date' => $request->shipment_date,
|
'shipment_date' => $request->shipment_date,
|
||||||
|
|
||||||
'total_ctn' => $total_ctn,
|
'total_ctn' => $total_ctn,
|
||||||
@@ -135,29 +149,35 @@ class ShipmentController extends Controller
|
|||||||
* Update Shipment status from action button
|
* Update Shipment status from action button
|
||||||
*/
|
*/
|
||||||
public function updateStatus(Request $request)
|
public function updateStatus(Request $request)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'shipment_id' => 'required|exists:shipments,id',
|
'shipment_id' => 'required|exists:shipments,id',
|
||||||
'status' => 'required|string'
|
'status' => 'required|string'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 1) Update shipment status
|
$shipment = Shipment::findOrFail($request->shipment_id);
|
||||||
$shipment = Shipment::findOrFail($request->shipment_id);
|
$shipment->status = $request->status;
|
||||||
$shipment->status = $request->status;
|
$shipment->save();
|
||||||
$shipment->save();
|
|
||||||
|
|
||||||
// 2) Update ALL related orders' status
|
// ✅ Sync shipment status to orders ONLY after shipment exists
|
||||||
foreach ($shipment->orders as $order) {
|
foreach ($shipment->orders as $order) {
|
||||||
$order->status = $shipment->status; // status is string: pending, in_transit, dispatched, delivered
|
|
||||||
$order->save();
|
// Prevent rollback or overwrite
|
||||||
|
if ($order->status === 'delivered') {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->back()->with(
|
$order->status = $shipment->status;
|
||||||
'success',
|
$order->save();
|
||||||
"Shipment status updated to {$shipment->statusLabel()} and related orders updated."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return redirect()->back()->with(
|
||||||
|
'success',
|
||||||
|
"Shipment status updated to {$shipment->statusLabel()}."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update shipment details
|
* Update shipment details
|
||||||
*/
|
*/
|
||||||
@@ -224,5 +244,95 @@ class ShipmentController extends Controller
|
|||||||
|
|
||||||
return view('admin.view_shipment', compact('shipment', 'dummyData'));
|
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()
|
public function index()
|
||||||
{
|
{
|
||||||
$requests = CustomerRequest::orderBy('id', 'desc')->get();
|
$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
|
// Approve user request
|
||||||
|
|||||||
577
app/Http/Controllers/ContainerController.php
Normal file
577
app/Http/Controllers/ContainerController.php
Normal file
@@ -0,0 +1,577 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK CHECK: strict - collect ALL marks + unmatched rows
|
||||||
|
$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));
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$unmatchedRowsData[] = [
|
||||||
|
'excel_row' => $headerRowIndex + 1 + $offset,
|
||||||
|
'mark_no' => $rowMark,
|
||||||
|
'data' => $rowData,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()
|
||||||
|
->withErrors(['excel_file' => 'Some mark numbers are not found in Mark List. Container not created.'])
|
||||||
|
->withInput()
|
||||||
|
->with('unmatched_rows', $unmatchedRowsData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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->invoice_number = $this->generateInvoiceNumber();
|
||||||
|
$invoice->invoice_date = now()->toDateString();
|
||||||
|
$invoice->due_date = null;
|
||||||
|
|
||||||
|
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'];
|
||||||
|
|
||||||
|
$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;
|
||||||
|
|
||||||
|
InvoiceItem::create([
|
||||||
|
'invoice_id' => $invoice->id,
|
||||||
|
'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;
|
||||||
|
|
||||||
|
$data = $row->data ?? [];
|
||||||
|
|
||||||
|
foreach ($cols as $colHeader => $value) {
|
||||||
|
$data[$colHeader] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$row->update([
|
||||||
|
'data' => $data,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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:pending,in-progress,completed,cancelled']);
|
||||||
|
|
||||||
|
$container->update(['status' => $request->status]);
|
||||||
|
|
||||||
|
return redirect()->route('containers.index')->with('success', 'Status updated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Container $container)
|
||||||
|
{
|
||||||
|
$container->delete();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,76 +6,48 @@ use Illuminate\Http\Request;
|
|||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
|
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class UserAuthController extends Controller
|
class UserAuthController extends Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
public function refreshToken()
|
public function refreshToken()
|
||||||
{
|
{
|
||||||
\Log::info('🔄 refreshToken() called');
|
Log::info('🔄 [JWT-REFRESH] called');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get current token
|
$newToken = JWTAuth::parseToken()->refresh();
|
||||||
$currentToken = JWTAuth::getToken();
|
|
||||||
|
|
||||||
if (!$currentToken) {
|
Log::info('✅ [JWT-REFRESH] Token refreshed');
|
||||||
\Log::warning('⚠ No token provided in refreshToken()');
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'Token not provided',
|
|
||||||
], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
\Log::info('📥 Current Token:', ['token' => (string) $currentToken]);
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'token' => $newToken,
|
||||||
|
]);
|
||||||
|
|
||||||
// Try refreshing token
|
} catch (\PHPOpenSourceSaver\JWTAuth\Exceptions\TokenExpiredException $e) {
|
||||||
$newToken = JWTAuth::refresh($currentToken);
|
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([
|
} catch (\Exception $e) {
|
||||||
'success' => true,
|
Log::error('🔥 [JWT-REFRESH] Exception', [
|
||||||
'token' => $newToken,
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} catch (\Tymon\JWTAuth\Exceptions\TokenExpiredException $e) {
|
return response()->json([
|
||||||
\Log::error('❌ TokenExpiredException in refreshToken()', [
|
'success' => false,
|
||||||
'message' => $e->getMessage(),
|
'message' => 'Unable to refresh token.',
|
||||||
]);
|
], 401);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User Login
|
* User Login
|
||||||
|
|||||||
103
app/Http/Controllers/user/ChatController.php
Normal file
103
app/Http/Controllers/user/ChatController.php
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<?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,
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
'client_id' => $request->client_id, // ✅ ADD
|
||||||
|
'read_by_admin' => false,
|
||||||
|
'read_by_user' => true,
|
||||||
|
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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
|
||||||
|
<<<<<<< HEAD
|
||||||
|
broadcast(new NewChatMessage($message))->toOthers();
|
||||||
|
=======
|
||||||
|
broadcast(new NewChatMessage($message));
|
||||||
|
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||||
|
|
||||||
|
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
|
class JwtRefreshMiddleware
|
||||||
{
|
{
|
||||||
public function handle($request, Closure $next)
|
public function handle($request, Closure $next)
|
||||||
{
|
{
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JWTAuth::parseToken()->authenticate();
|
JWTAuth::parseToken()->authenticate();
|
||||||
} catch (TokenExpiredException $e) {
|
} 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',
|
'name', 'email', 'password', 'username',
|
||||||
'phone', 'emergency_phone', 'address',
|
'phone', 'emergency_phone', 'address',
|
||||||
'role', 'department', 'designation', 'joining_date',
|
'role', 'department', 'designation', 'joining_date',
|
||||||
'status', 'additional_info', 'type', // admin/staff indicator
|
'status', 'additional_info', 'type','employee_id', // admin/staff indicator
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
|
|||||||
50
app/Models/ChatMessage.php
Normal file
50
app/Models/ChatMessage.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
class ChatMessage extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
<<<<<<< HEAD
|
||||||
|
'ticket_id',
|
||||||
|
'sender_id',
|
||||||
|
'sender_type', // user OR admin
|
||||||
|
'message',
|
||||||
|
'file_path',
|
||||||
|
'file_type',
|
||||||
|
];
|
||||||
|
=======
|
||||||
|
'ticket_id',
|
||||||
|
'sender_id',
|
||||||
|
'sender_type',
|
||||||
|
'message',
|
||||||
|
'file_path',
|
||||||
|
'file_type',
|
||||||
|
'read_by_admin',
|
||||||
|
'read_by_user',
|
||||||
|
'client_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,30 @@ class Invoice extends Model
|
|||||||
{
|
{
|
||||||
use HasFactory;
|
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
|
* Relationships
|
||||||
@@ -54,16 +43,28 @@ class Invoice extends Model
|
|||||||
return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC');
|
return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function order()
|
// NEW: invoice आता container वर depend
|
||||||
|
public function container()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Order::class);
|
return $this->belongsTo(Container::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OLD: order() relation काढले आहे
|
||||||
|
// public function order()
|
||||||
|
// {
|
||||||
|
// return $this->belongsTo(Order::class);
|
||||||
|
// }
|
||||||
|
|
||||||
public function customer()
|
public function customer()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class, 'customer_id');
|
return $this->belongsTo(User::class, 'customer_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function installments()
|
||||||
|
{
|
||||||
|
return $this->hasMany(InvoiceInstallment::class);
|
||||||
|
}
|
||||||
|
|
||||||
/****************************
|
/****************************
|
||||||
* Helper Functions
|
* Helper Functions
|
||||||
****************************/
|
****************************/
|
||||||
@@ -72,7 +73,7 @@ class Invoice extends Model
|
|||||||
public function calculateTotals()
|
public function calculateTotals()
|
||||||
{
|
{
|
||||||
$gst = ($this->final_amount * $this->gst_percent) / 100;
|
$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;
|
$this->final_amount_with_gst = $this->final_amount + $gst;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,15 +83,10 @@ class Invoice extends Model
|
|||||||
return $this->status === 'pending' && now()->gt($this->due_date);
|
return $this->status === 'pending' && now()->gt($this->due_date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// जर पुढे container → shipment relation असेल तर हा helper नंतर adjust करू
|
||||||
public function getShipment()
|
public function getShipment()
|
||||||
{
|
{
|
||||||
return $this->order?->shipments?->first();
|
// आधी order वरून shipment घेत होत; container flow मध्ये नंतर गरज पडल्यास बदलू
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function installments()
|
|
||||||
{
|
|
||||||
return $this->hasMany(InvoiceInstallment::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
28
app/Models/LoadingListItem.php
Normal file
28
app/Models/LoadingListItem.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class LoadingListItem extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'container_id',
|
||||||
|
'mark',
|
||||||
|
'description',
|
||||||
|
'ctn',
|
||||||
|
'qty',
|
||||||
|
'total_qty',
|
||||||
|
'unit',
|
||||||
|
'price',
|
||||||
|
'cbm',
|
||||||
|
'total_cbm',
|
||||||
|
'kg',
|
||||||
|
'total_kg',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function container()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Container::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,5 +64,25 @@ class Order extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 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');
|
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
|
// HELPERS
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@@ -73,8 +54,38 @@ class Shipment extends Model
|
|||||||
return $this->items()->count();
|
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()
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -94,6 +94,19 @@ public function invoices()
|
|||||||
return $this->hasMany(\App\Models\Invoice::class, 'customer_id', 'id');
|
return $this->hasMany(\App\Models\Invoice::class, 'customer_id', 'id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// App\Models\User.php
|
||||||
|
|
||||||
|
|
||||||
|
public function invoiceInstallments()
|
||||||
|
{
|
||||||
|
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__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->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',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
health: '/up',
|
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\AppServiceProvider::class,
|
||||||
App\Providers\RouteServiceProvider::class,
|
App\Providers\RouteServiceProvider::class,
|
||||||
App\Providers\AuthServiceProvider::class,
|
App\Providers\AuthServiceProvider::class,
|
||||||
|
App\Providers\BroadcastServiceProvider::class,
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -9,8 +9,9 @@
|
|||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"barryvdh/laravel-dompdf": "^3.1",
|
"barryvdh/laravel-dompdf": "^3.1",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
|
"laravel/reverb": "^1.6",
|
||||||
"laravel/tinker": "^2.10.1",
|
"laravel/tinker": "^2.10.1",
|
||||||
"maatwebsite/excel": "^1.1",
|
"maatwebsite/excel": "^3.1",
|
||||||
"mpdf/mpdf": "^8.2",
|
"mpdf/mpdf": "^8.2",
|
||||||
"php-open-source-saver/jwt-auth": "2.8",
|
"php-open-source-saver/jwt-auth": "2.8",
|
||||||
"spatie/laravel-permission": "^6.23"
|
"spatie/laravel-permission": "^6.23"
|
||||||
@@ -84,7 +85,8 @@
|
|||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
"pestphp/pest-plugin": true,
|
"pestphp/pest-plugin": true,
|
||||||
"php-http/discovery": true
|
"php-http/discovery": true
|
||||||
}
|
},
|
||||||
|
"platform-check": false
|
||||||
},
|
},
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"prefer-stable": true
|
"prefer-stable": true
|
||||||
|
|||||||
1626
composer.lock
generated
1626
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -6,11 +6,6 @@ return [
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Application Name
|
| Application Name
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
| This value is the name of your application, which will be used when the
|
|
||||||
| framework needs to place the application's name in a notification or
|
|
||||||
| other UI elements where an application name needs to be displayed.
|
|
||||||
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'name' => env('APP_NAME', 'Laravel'),
|
'name' => env('APP_NAME', 'Laravel'),
|
||||||
@@ -19,11 +14,6 @@ return [
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Application Environment
|
| Application Environment
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
| This value determines the "environment" your application is currently
|
|
||||||
| running in. This may determine how you prefer to configure various
|
|
||||||
| services the application utilizes. Set this in your ".env" file.
|
|
||||||
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'env' => env('APP_ENV', 'production'),
|
'env' => env('APP_ENV', 'production'),
|
||||||
@@ -32,11 +22,6 @@ return [
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Application Debug Mode
|
| Application Debug Mode
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
| When your application is in debug mode, detailed error messages with
|
|
||||||
| stack traces will be shown on every error that occurs within your
|
|
||||||
| application. If disabled, a simple generic error page is shown.
|
|
||||||
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'debug' => (bool) env('APP_DEBUG', false),
|
'debug' => (bool) env('APP_DEBUG', false),
|
||||||
@@ -45,11 +30,6 @@ return [
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Application URL
|
| Application URL
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
| This URL is used by the console to properly generate URLs when using
|
|
||||||
| the Artisan command line tool. You should set this to the root of
|
|
||||||
| the application so that it's available within Artisan commands.
|
|
||||||
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'url' => env('APP_URL', 'http://localhost'),
|
'url' => env('APP_URL', 'http://localhost'),
|
||||||
@@ -58,11 +38,6 @@ return [
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Application Timezone
|
| Application Timezone
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
| Here you may specify the default timezone for your application, which
|
|
||||||
| will be used by the PHP date and date-time functions. The timezone
|
|
||||||
| is set to "UTC" by default as it is suitable for most use cases.
|
|
||||||
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'timezone' => 'UTC',
|
'timezone' => 'UTC',
|
||||||
@@ -71,11 +46,6 @@ return [
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Application Locale Configuration
|
| Application Locale Configuration
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
| The application locale determines the default locale that will be used
|
|
||||||
| by Laravel's translation / localization methods. This option can be
|
|
||||||
| set to any locale for which you plan to have translation strings.
|
|
||||||
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'locale' => env('APP_LOCALE', 'en'),
|
'locale' => env('APP_LOCALE', 'en'),
|
||||||
@@ -88,11 +58,6 @@ return [
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Encryption Key
|
| Encryption Key
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
| This key is utilized by Laravel's encryption services and should be set
|
|
||||||
| to a random, 32 character string to ensure that all encrypted values
|
|
||||||
| are secure. You should do this prior to deploying the application.
|
|
||||||
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'cipher' => 'AES-256-CBC',
|
'cipher' => 'AES-256-CBC',
|
||||||
@@ -109,13 +74,6 @@ return [
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Maintenance Mode Driver
|
| Maintenance Mode Driver
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
| These configuration options determine the driver used to determine and
|
|
||||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
|
||||||
| allow maintenance mode to be controlled across multiple machines.
|
|
||||||
|
|
|
||||||
| Supported drivers: "file", "cache"
|
|
||||||
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'maintenance' => [
|
'maintenance' => [
|
||||||
@@ -123,4 +81,53 @@ return [
|
|||||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Class Aliases
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
'aliases' => [
|
||||||
|
|
||||||
|
'App' => Illuminate\Support\Facades\App::class,
|
||||||
|
'Arr' => Illuminate\Support\Arr::class,
|
||||||
|
'Artisan' => Illuminate\Support\Facades\Artisan::class,
|
||||||
|
'Auth' => Illuminate\Support\Facades\Auth::class,
|
||||||
|
'Blade' => Illuminate\Support\Facades\Blade::class,
|
||||||
|
'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
|
||||||
|
'Bus' => Illuminate\Support\Facades\Bus::class,
|
||||||
|
'Cache' => Illuminate\Support\Facades\Cache::class,
|
||||||
|
'Config' => Illuminate\Support\Facades\Config::class,
|
||||||
|
'Cookie' => Illuminate\Support\Facades\Cookie::class,
|
||||||
|
'Crypt' => Illuminate\Support\Facades\Crypt::class,
|
||||||
|
'DB' => Illuminate\Support\Facades\DB::class,
|
||||||
|
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
|
||||||
|
'Event' => Illuminate\Support\Facades\Event::class,
|
||||||
|
'File' => Illuminate\Support\Facades\File::class,
|
||||||
|
'Gate' => Illuminate\Support\Facades\Gate::class,
|
||||||
|
'Hash' => Illuminate\Support\Facades\Hash::class,
|
||||||
|
'Http' => Illuminate\Support\Facades\Http::class,
|
||||||
|
'Lang' => Illuminate\Support\Facades\Lang::class,
|
||||||
|
'Log' => Illuminate\Support\Facades\Log::class,
|
||||||
|
'Mail' => Illuminate\Support\Facades\Mail::class,
|
||||||
|
'Notification' => Illuminate\Support\Facades\Notification::class,
|
||||||
|
'Password' => Illuminate\Support\Facades\Password::class,
|
||||||
|
'Queue' => Illuminate\Support\Facades\Queue::class,
|
||||||
|
'RateLimiter' => Illuminate\Support\Facades\RateLimiter::class,
|
||||||
|
'Redirect' => Illuminate\Support\Facades\Redirect::class,
|
||||||
|
'Request' => Illuminate\Support\Facades\Request::class,
|
||||||
|
'Response' => Illuminate\Support\Facades\Response::class,
|
||||||
|
'Route' => Illuminate\Support\Facades\Route::class,
|
||||||
|
'Schema' => Illuminate\Support\Facades\Schema::class,
|
||||||
|
'Session' => Illuminate\Support\Facades\Session::class,
|
||||||
|
'Storage' => Illuminate\Support\Facades\Storage::class,
|
||||||
|
'Str' => Illuminate\Support\Str::class,
|
||||||
|
'URL' => Illuminate\Support\Facades\URL::class,
|
||||||
|
'Validator' => Illuminate\Support\Facades\Validator::class,
|
||||||
|
'View' => Illuminate\Support\Facades\View::class,
|
||||||
|
|
||||||
|
// ✅ Laravel‑Excel facade
|
||||||
|
'Excel' => Maatwebsite\Excel\Facades\Excel::class,
|
||||||
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ return [
|
|||||||
'driver' => 'eloquent',
|
'driver' => 'eloquent',
|
||||||
'model' => App\Models\Staff::class,
|
'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',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
380
config/excel.php
Normal file
380
config/excel.php
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Maatwebsite\Excel\Excel;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Reader\Csv;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'exports' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Chunk size
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using FromQuery, the query is automatically chunked.
|
||||||
|
| Here you can specify how big the chunk should be.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'chunk_size' => 1000,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Pre-calculate formulas during export
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
'pre_calculate_formulas' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Enable strict null comparison
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When enabling strict null comparison empty cells ('') will
|
||||||
|
| be added to the sheet.
|
||||||
|
*/
|
||||||
|
'strict_null_comparison' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| CSV Settings
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure e.g. delimiter, enclosure and line ending for CSV exports.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'csv' => [
|
||||||
|
'delimiter' => ',',
|
||||||
|
'enclosure' => '"',
|
||||||
|
'line_ending' => PHP_EOL,
|
||||||
|
'use_bom' => false,
|
||||||
|
'include_separator_line' => false,
|
||||||
|
'excel_compatibility' => false,
|
||||||
|
'output_encoding' => '',
|
||||||
|
'test_auto_detect' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Worksheet properties
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure e.g. default title, creator, subject,...
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'properties' => [
|
||||||
|
'creator' => '',
|
||||||
|
'lastModifiedBy' => '',
|
||||||
|
'title' => '',
|
||||||
|
'description' => '',
|
||||||
|
'subject' => '',
|
||||||
|
'keywords' => '',
|
||||||
|
'category' => '',
|
||||||
|
'manager' => '',
|
||||||
|
'company' => '',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'imports' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Read Only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When dealing with imports, you might only be interested in the
|
||||||
|
| data that the sheet exists. By default we ignore all styles,
|
||||||
|
| however if you want to do some logic based on style data
|
||||||
|
| you can enable it by setting read_only to false.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'read_only' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Ignore Empty
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When dealing with imports, you might be interested in ignoring
|
||||||
|
| rows that have null values or empty strings. By default rows
|
||||||
|
| containing empty strings or empty values are not ignored but can be
|
||||||
|
| ignored by enabling the setting ignore_empty to true.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'ignore_empty' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Heading Row Formatter
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure the heading row formatter.
|
||||||
|
| Available options: none|slug|custom
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'heading_row' => [
|
||||||
|
'formatter' => 'slug',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| CSV Settings
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure e.g. delimiter, enclosure and line ending for CSV imports.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'csv' => [
|
||||||
|
'delimiter' => null,
|
||||||
|
'enclosure' => '"',
|
||||||
|
'escape_character' => '\\',
|
||||||
|
'contiguous' => false,
|
||||||
|
'input_encoding' => Csv::GUESS_ENCODING,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Worksheet properties
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure e.g. default title, creator, subject,...
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'properties' => [
|
||||||
|
'creator' => '',
|
||||||
|
'lastModifiedBy' => '',
|
||||||
|
'title' => '',
|
||||||
|
'description' => '',
|
||||||
|
'subject' => '',
|
||||||
|
'keywords' => '',
|
||||||
|
'category' => '',
|
||||||
|
'manager' => '',
|
||||||
|
'company' => '',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cell Middleware
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure middleware that is executed on getting a cell value
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'cells' => [
|
||||||
|
'middleware' => [
|
||||||
|
//\Maatwebsite\Excel\Middleware\TrimCellValue::class,
|
||||||
|
//\Maatwebsite\Excel\Middleware\ConvertEmptyCellValuesToNull::class,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Extension detector
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure here which writer/reader type should be used when the package
|
||||||
|
| needs to guess the correct type based on the extension alone.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'extension_detector' => [
|
||||||
|
'xlsx' => Excel::XLSX,
|
||||||
|
'xlsm' => Excel::XLSX,
|
||||||
|
'xltx' => Excel::XLSX,
|
||||||
|
'xltm' => Excel::XLSX,
|
||||||
|
'xls' => Excel::XLS,
|
||||||
|
'xlt' => Excel::XLS,
|
||||||
|
'ods' => Excel::ODS,
|
||||||
|
'ots' => Excel::ODS,
|
||||||
|
'slk' => Excel::SLK,
|
||||||
|
'xml' => Excel::XML,
|
||||||
|
'gnumeric' => Excel::GNUMERIC,
|
||||||
|
'htm' => Excel::HTML,
|
||||||
|
'html' => Excel::HTML,
|
||||||
|
'csv' => Excel::CSV,
|
||||||
|
'tsv' => Excel::TSV,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| PDF Extension
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure here which Pdf driver should be used by default.
|
||||||
|
| Available options: Excel::MPDF | Excel::TCPDF | Excel::DOMPDF
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'pdf' => Excel::DOMPDF,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Value Binder
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| PhpSpreadsheet offers a way to hook into the process of a value being
|
||||||
|
| written to a cell. In there some assumptions are made on how the
|
||||||
|
| value should be formatted. If you want to change those defaults,
|
||||||
|
| you can implement your own default value binder.
|
||||||
|
|
|
||||||
|
| Possible value binders:
|
||||||
|
|
|
||||||
|
| [x] Maatwebsite\Excel\DefaultValueBinder::class
|
||||||
|
| [x] PhpOffice\PhpSpreadsheet\Cell\StringValueBinder::class
|
||||||
|
| [x] PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder::class
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'value_binder' => [
|
||||||
|
'default' => Maatwebsite\Excel\DefaultValueBinder::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default cell caching driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By default PhpSpreadsheet keeps all cell values in memory, however when
|
||||||
|
| dealing with large files, this might result into memory issues. If you
|
||||||
|
| want to mitigate that, you can configure a cell caching driver here.
|
||||||
|
| When using the illuminate driver, it will store each value in the
|
||||||
|
| cache store. This can slow down the process, because it needs to
|
||||||
|
| store each value. You can use the "batch" store if you want to
|
||||||
|
| only persist to the store when the memory limit is reached.
|
||||||
|
|
|
||||||
|
| Drivers: memory|illuminate|batch
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'driver' => 'memory',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Batch memory caching
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When dealing with the "batch" caching driver, it will only
|
||||||
|
| persist to the store when the memory limit is reached.
|
||||||
|
| Here you can tweak the memory limit to your liking.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'batch' => [
|
||||||
|
'memory_limit' => 60000,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Illuminate cache
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "illuminate" caching driver, it will automatically use
|
||||||
|
| your default cache store. However if you prefer to have the cell
|
||||||
|
| cache on a separate store, you can configure the store name here.
|
||||||
|
| You can use any store defined in your cache config. When leaving
|
||||||
|
| at "null" it will use the default store.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'illuminate' => [
|
||||||
|
'store' => null,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Time-to-live (TTL)
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The TTL of items written to cache. If you want to keep the items cached
|
||||||
|
| indefinitely, set this to null. Otherwise, set a number of seconds,
|
||||||
|
| a \DateInterval, or a callable.
|
||||||
|
|
|
||||||
|
| Allowable types: callable|\DateInterval|int|null
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'default_ttl' => 10800,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Transaction Handler
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By default the import is wrapped in a transaction. This is useful
|
||||||
|
| for when an import may fail and you want to retry it. With the
|
||||||
|
| transactions, the previous import gets rolled-back.
|
||||||
|
|
|
||||||
|
| You can disable the transaction handler by setting this to null.
|
||||||
|
| Or you can choose a custom made transaction handler here.
|
||||||
|
|
|
||||||
|
| Supported handlers: null|db
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'transactions' => [
|
||||||
|
'handler' => 'db',
|
||||||
|
'db' => [
|
||||||
|
'connection' => null,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'temporary_files' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Local Temporary Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When exporting and importing files, we use a temporary file, before
|
||||||
|
| storing reading or downloading. Here you can customize that path.
|
||||||
|
| permissions is an array with the permission flags for the directory (dir)
|
||||||
|
| and the create file (file).
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'local_path' => storage_path('framework/cache/laravel-excel'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Local Temporary Path Permissions
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Permissions is an array with the permission flags for the directory (dir)
|
||||||
|
| and the create file (file).
|
||||||
|
| If omitted the default permissions of the filesystem will be used.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'local_permissions' => [
|
||||||
|
// 'dir' => 0755,
|
||||||
|
// 'file' => 0644,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Remote Temporary Disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When dealing with a multi server setup with queues in which you
|
||||||
|
| cannot rely on having a shared local temporary path, you might
|
||||||
|
| want to store the temporary file on a shared disk. During the
|
||||||
|
| queue executing, we'll retrieve the temporary file from that
|
||||||
|
| location instead. When left to null, it will always use
|
||||||
|
| the local path. This setting only has effect when using
|
||||||
|
| in conjunction with queued imports and exports.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'remote_disk' => null,
|
||||||
|
'remote_prefix' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Force Resync
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When dealing with a multi server setup as above, it's possible
|
||||||
|
| for the clean up that occurs after entire queue has been run to only
|
||||||
|
| cleanup the server that the last AfterImportJob runs on. The rest of the server
|
||||||
|
| would still have the local temporary file stored on it. In this case your
|
||||||
|
| local storage limits can be exceeded and future imports won't be processed.
|
||||||
|
| To mitigate this you can set this config value to be true, so that after every
|
||||||
|
| queued chunk is processed the local temporary file is deleted on the server that
|
||||||
|
| processed it.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'force_resync_remote' => 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),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
@@ -13,16 +13,28 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
Schema::create('chat_messages', function (Blueprint $table) {
|
Schema::create('chat_messages', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->unsignedBigInteger('ticket_id'); // support ticket ID
|
|
||||||
$table->unsignedBigInteger('sender_id'); // user or admin/staff
|
// Chat belongs to a ticket
|
||||||
$table->text('message')->nullable(); // message content
|
$table->unsignedBigInteger('ticket_id');
|
||||||
$table->string('file_path')->nullable(); // image/pdf/video
|
|
||||||
$table->string('file_type')->default('text'); // text/image/pdf/video
|
// 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();
|
$table->timestamps();
|
||||||
|
|
||||||
// foreign keys
|
// FK to tickets table
|
||||||
$table->foreign('ticket_id')->references('id')->on('support_tickets')->onDelete('cascade');
|
$table->foreign('ticket_id')
|
||||||
$table->foreign('sender_id')->references('id')->on('users')->onDelete('cascade'); // admin also stored in users table? If admin separate, change later.
|
->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', 20)
|
||||||
|
->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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
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",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"vite": "^7.0.7"
|
"vite": "^7.0.7"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"laravel-echo": "^2.2.6",
|
||||||
|
"pusher-js": "^8.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
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';
|
import axios from 'axios';
|
||||||
window.axios = 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;
|
||||||
|
|
||||||
|
|||||||
31
resources/js/echo.js
Normal file
31
resources/js/echo.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import Echo from 'laravel-echo';
|
||||||
|
import Pusher from 'pusher-js';
|
||||||
|
|
||||||
|
window.Pusher = Pusher;
|
||||||
|
|
||||||
|
// Get CSRF token from meta tag
|
||||||
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||||
|
window.Echo = new Echo({
|
||||||
|
broadcaster: 'reverb',
|
||||||
|
key: import.meta.env.VITE_REVERB_APP_KEY,
|
||||||
|
wsHost: import.meta.env.VITE_REVERB_HOST,
|
||||||
|
wsPort: import.meta.env.VITE_REVERB_PORT ?? 8080,
|
||||||
|
wssPort: import.meta.env.VITE_REVERB_PORT ?? 8080,
|
||||||
|
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'http') === 'https',
|
||||||
|
enabledTransports: ['ws', 'wss'],
|
||||||
|
|
||||||
|
authEndpoint: '/admin/broadcasting/auth',
|
||||||
|
|
||||||
|
|
||||||
|
// ⭐ MOST IMPORTANT ⭐
|
||||||
|
withCredentials: true,
|
||||||
|
|
||||||
|
auth: {
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': csrfToken,
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('%c[ECHO] Initialized!', 'color: green; font-weight: bold;', window.Echo);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,169 @@
|
|||||||
@extends('admin.layouts.app')
|
@extends('admin.layouts.app')
|
||||||
|
|
||||||
@section('page-title', 'Dashboard')
|
@section('page-title', 'Chat Support')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="card shadow-sm">
|
|
||||||
<div class="card-body">
|
<div class="container py-4">
|
||||||
<h4>Welcome to the Admin chat</h4>
|
|
||||||
<p>Here you can manage all system modules.</p>
|
<h2 class="mb-4 fw-bold">Customer Support Chat</h2>
|
||||||
</div>
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
|
||||||
|
@if($tickets->count() === 0)
|
||||||
|
<div class="p-4 text-center text-muted">
|
||||||
|
<h5>No customer chats yet.</h5>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
|
||||||
|
@foreach($tickets as $ticket)
|
||||||
|
@php
|
||||||
|
// Get last message
|
||||||
|
$lastMsg = $ticket->messages()->latest()->first();
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<li class="list-group-item py-3">
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
|
||||||
|
<!-- Left side: User info + last message -->
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
|
||||||
|
<!-- Profile Circle -->
|
||||||
|
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center"
|
||||||
|
style="width: 45px; height: 45px; font-size: 18px;">
|
||||||
|
{{ strtoupper(substr($ticket->user->customer_name ?? $ticket->user->name, 0, 1)) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<!-- Customer Name -->
|
||||||
|
<h6 class="mb-1 fw-semibold">
|
||||||
|
{{ $ticket->user->customer_name ?? $ticket->user->name }}
|
||||||
|
</h6>
|
||||||
|
|
||||||
|
<!-- Last message preview -->
|
||||||
|
<small class="text-muted">
|
||||||
|
@if($lastMsg)
|
||||||
|
@if($lastMsg->message)
|
||||||
|
{{ Str::limit($lastMsg->message, 35) }}
|
||||||
|
@elseif($lastMsg->file_type === 'image')
|
||||||
|
📷 Image
|
||||||
|
@elseif($lastMsg->file_type === 'video')
|
||||||
|
🎥 Video
|
||||||
|
@else
|
||||||
|
📎 Attachment
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<i>No messages yet</i>
|
||||||
|
@endif
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Side: Status + Button -->
|
||||||
|
<div class="text-end">
|
||||||
|
|
||||||
|
<!-- Ticket Status -->
|
||||||
|
<span class="badge
|
||||||
|
{{ $ticket->status === 'open' ? 'bg-success' : 'bg-danger' }}">
|
||||||
|
{{ ucfirst($ticket->status) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Open Chat Button -->
|
||||||
|
<a href="{{ route('admin.chat.open', $ticket->id) }}"
|
||||||
|
class="btn btn-sm btn-primary ms-2">
|
||||||
|
Open Chat →
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</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
|
@endsection
|
||||||
|
|||||||
200
resources/views/admin/chat_window.blade.php
Normal file
200
resources/views/admin/chat_window.blade.php
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
@extends('admin.layouts.app')
|
||||||
|
|
||||||
|
@section('page-title', 'Chat With ' . ($ticket->user->customer_name ?? $ticket->user->name))
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chat-box {
|
||||||
|
height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #f5f6fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
.message {
|
||||||
|
max-width: 65%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.message.admin {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
margin-left: auto;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
.message.user {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
margin-right: auto;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
.chat-input {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 15px;
|
||||||
|
left: 250px;
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container py-4">
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<h4 class="fw-bold mb-0">
|
||||||
|
Chat With: {{ $ticket->user->customer_name ?? $ticket->user->name }}
|
||||||
|
</h4>
|
||||||
|
<span class="badge ms-3 {{ $ticket->status === 'open' ? 'bg-success' : 'bg-danger' }}">
|
||||||
|
{{ ucfirst($ticket->status) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="chatBox" class="chat-box border shadow-sm">
|
||||||
|
|
||||||
|
@foreach($messages as $msg)
|
||||||
|
<div class="message {{ $msg->sender_type === 'App\\Models\\Admin' ? 'admin' : 'user' }}">
|
||||||
|
|
||||||
|
{{-- TEXT --}}
|
||||||
|
@if($msg->message)
|
||||||
|
<div>{{ $msg->message }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- FILE --}}
|
||||||
|
@if($msg->file_path)
|
||||||
|
<div class="mt-2">
|
||||||
|
@php $isImage = Str::startsWith($msg->file_type, 'image'); @endphp
|
||||||
|
|
||||||
|
@if($isImage)
|
||||||
|
<img src="{{ asset('storage/'.$msg->file_path) }}" style="max-width:150px;" class="rounded">
|
||||||
|
@else
|
||||||
|
<a href="{{ asset('storage/'.$msg->file_path) }}" target="_blank">📎 View Attachment</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<small class="text-muted d-block mt-1">
|
||||||
|
{{ $msg->created_at->format('d M h:i A') }}
|
||||||
|
</small>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-input">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body d-flex align-items-center gap-2">
|
||||||
|
<input type="text" id="messageInput" class="form-control" placeholder="Type your message...">
|
||||||
|
<input type="file" id="fileInput" class="form-control" style="max-width:200px;">
|
||||||
|
<button class="btn btn-primary" id="sendBtn">Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
<script>
|
||||||
|
console.log("CHAT WINDOW: script loaded");
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// WAIT FOR ECHO READY
|
||||||
|
// -------------------------------
|
||||||
|
function waitForEcho(callback, retries = 40) {
|
||||||
|
if (window.Echo) {
|
||||||
|
console.log("%c[ECHO] Ready!", "color: green; font-weight: bold;", window.Echo);
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn("[ECHO] Not ready. Retrying...");
|
||||||
|
if (retries <= 0) {
|
||||||
|
console.error("[ECHO] FAILED to initialize after retry limit");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => waitForEcho(callback, retries - 1), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll chat down
|
||||||
|
function scrollToBottom() {
|
||||||
|
const el = document.getElementById("chatBox");
|
||||||
|
if (el) el.scrollTop = el.scrollHeight;
|
||||||
|
}
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// SEND MESSAGE (WORKING PART FROM SCRIPT #1)
|
||||||
|
// -------------------------------
|
||||||
|
document.getElementById("sendBtn").addEventListener("click", function () {
|
||||||
|
console.log("[SEND] Attempting to send message...");
|
||||||
|
|
||||||
|
let msg = document.getElementById("messageInput").value;
|
||||||
|
let file = document.getElementById("fileInput").files[0];
|
||||||
|
|
||||||
|
if (!msg.trim() && !file) {
|
||||||
|
alert("Please type something or upload a file.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append("message", msg);
|
||||||
|
if (file) formData.append("file", file);
|
||||||
|
|
||||||
|
fetch("{{ route('admin.chat.send', $ticket->id) }}", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "X-CSRF-TOKEN": "{{ csrf_token() }}" },
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((response) => {
|
||||||
|
console.log("[SEND] Message sent:", response);
|
||||||
|
document.getElementById("messageInput").value = "";
|
||||||
|
document.getElementById("fileInput").value = "";
|
||||||
|
})
|
||||||
|
.catch(err => console.error("[SEND] Error:", err));
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// LISTEN FOR REALTIME MESSAGE (WORKING PART FROM SCRIPT #2)
|
||||||
|
// -------------------------------
|
||||||
|
waitForEcho(() => {
|
||||||
|
const ticketId = "{{ $ticket->id }}";
|
||||||
|
|
||||||
|
console.log("[ECHO] Subscribing to channel:", `ticket.${ticketId}`);
|
||||||
|
|
||||||
|
window.Echo.private(`ticket.${ticketId}`)
|
||||||
|
.listen("NewChatMessage", (event) => {
|
||||||
|
|
||||||
|
console.log("%c[REALTIME RECEIVED]", "color: blue; font-weight: bold;", event);
|
||||||
|
|
||||||
|
const msg = event.message;
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="message ${msg.sender_type === 'App\\Models\\Admin' ? 'admin' : 'user'}">
|
||||||
|
${msg.message ?? ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (msg.file_url) {
|
||||||
|
if (msg.file_type.startsWith("image")) {
|
||||||
|
html += `<img src="${msg.file_url}" class="rounded mt-2" style="max-width:150px;">`;
|
||||||
|
} else {
|
||||||
|
html += `<a href="${msg.file_url}" target="_blank" class="mt-2 d-block">📎 View File</a>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<small class="text-muted d-block mt-1">Just now</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById("chatBox").innerHTML += html;
|
||||||
|
scrollToBottom();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
710
resources/views/admin/container.blade.php
Normal file
710
resources/views/admin/container.blade.php
Normal file
@@ -0,0 +1,710 @@
|
|||||||
|
@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-badge {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge i {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-in-progress {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-completed {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cancelled {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--dark-text);
|
||||||
|
background: white;
|
||||||
|
min-width: 140px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-btn {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-btn:hover {
|
||||||
|
background: #3b5de6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 */
|
||||||
|
.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 {
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.status-select, .update-btn {
|
||||||
|
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>
|
||||||
|
<a href="{{ route('containers.create') }}" class="add-container-btn">
|
||||||
|
<i class="fas fa-plus-circle"></i>
|
||||||
|
Add New Container
|
||||||
|
</a>
|
||||||
|
</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="pending" {{ request('status') == 'pending' ? 'selected' : '' }}>Pending</option>
|
||||||
|
<option value="in-progress" {{ request('status') == 'in-progress' ? 'selected' : '' }}>In Progress</option>
|
||||||
|
<option value="completed" {{ request('status') == 'completed' ? 'selected' : '' }}>Completed</option>
|
||||||
|
<option value="cancelled" {{ request('status') == 'cancelled' ? 'selected' : '' }}>Cancelled</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
|
||||||
|
@foreach($containers as $container)
|
||||||
|
@php
|
||||||
|
$status = $container->status;
|
||||||
|
$statusClass = match ($status) {
|
||||||
|
'completed' => 'status-completed',
|
||||||
|
'in-progress' => 'status-in-progress',
|
||||||
|
'cancelled' => 'status-cancelled',
|
||||||
|
default => 'status-pending',
|
||||||
|
};
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="container-item">
|
||||||
|
<div class="container-header">
|
||||||
|
<div class="container-info">
|
||||||
|
<div class="container-avatar">
|
||||||
|
{{ 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">
|
||||||
|
<span class="status-badge {{ $statusClass }}">
|
||||||
|
<i class="fas fa-circle"></i>
|
||||||
|
{{ ucfirst(str_replace('-', ' ', $status)) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<a href="{{ route('containers.show', $container->id) }}" class="action-btn view-btn">
|
||||||
|
<i class="fas fa-eye"></i> View
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<form action="{{ route('containers.update-status', $container->id) }}"
|
||||||
|
method="POST" class="update-form">
|
||||||
|
@csrf
|
||||||
|
<select name="status" class="status-select">
|
||||||
|
<option value="pending" {{ $status === 'pending' ? 'selected' : '' }}>Pending</option>
|
||||||
|
<option value="in-progress" {{ $status === 'in-progress' ? 'selected' : '' }}>In Progress</option>
|
||||||
|
<option value="completed" {{ $status === 'completed' ? 'selected' : '' }}>Completed</option>
|
||||||
|
<option value="cancelled" {{ $status === 'cancelled' ? 'selected' : '' }}>Cancelled</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="update-btn">
|
||||||
|
<i class="fas fa-sync-alt"></i> Update
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form action="{{ route('containers.destroy', $container->id) }}" method="POST"
|
||||||
|
onsubmit="return confirm('Are you sure you want to delete this container and all its entries?');">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="action-btn delete-btn">
|
||||||
|
<i class="fas fa-trash"></i> Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 🔥 Totals instead of first row preview -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
|
||||||
|
@endsection
|
||||||
251
resources/views/admin/container_create.blade.php
Normal file
251
resources/views/admin/container_create.blade.php
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-error-list {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container-fluid cm-add-wrapper">
|
||||||
|
|
||||||
|
{{-- TOP GRADIENT HEADER --}}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{{-- MAIN CARD --}}
|
||||||
|
<div class="card cm-add-main-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Add Container</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
{{-- SUCCESS MESSAGE --}}
|
||||||
|
@if (session('success'))
|
||||||
|
<div class="alert alert-success">
|
||||||
|
{{ session('success') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- VALIDATION ERRORS --}}
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<ul class="mb-0 cm-error-list">
|
||||||
|
@foreach ($errors->all() as $error)
|
||||||
|
<li>{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- UNMATCHED ROWS TABLE --}}
|
||||||
|
@if (session('unmatched_rows'))
|
||||||
|
<div class="alert alert-warning mt-3">
|
||||||
|
<strong>Mark number not matched:</strong>
|
||||||
|
|
||||||
|
|
||||||
|
@php
|
||||||
|
$unmatchedRows = session('unmatched_rows');
|
||||||
|
$headings = [];
|
||||||
|
if (!empty($unmatchedRows)) {
|
||||||
|
$headings = array_keys($unmatchedRows[0]['data'] ?? []);
|
||||||
|
// इथे Excel मधला 'MARK' कॉलम hide करतो, कारण आधीच Mark No वेगळा column आहे
|
||||||
|
$headings = array_filter($headings, function ($h) {
|
||||||
|
return strtoupper(trim($h)) !== 'MARK';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if(!empty($unmatchedRows))
|
||||||
|
<div class="table-responsive" style="max-height:260px; overflow:auto; border:1px solid #e3e6ef;">
|
||||||
|
<table class="table table-sm table-bordered mb-0" style="font-size:11.5px; min-width:800px;">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Excel Row</th>
|
||||||
|
<th>Mark No</th>
|
||||||
|
@foreach($headings as $head)
|
||||||
|
<th>{{ $head }}</th>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($unmatchedRows as $row)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $row['excel_row'] }}</td>
|
||||||
|
<td>{{ $row['mark_no'] }}</td>
|
||||||
|
@foreach($headings as $head)
|
||||||
|
<td>{{ $row['data'][$head] ?? '' }}</td>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- FORM: unmatched_rows असल्यावर form लपवायचा असेल तर खालील condition ठेवा --}}
|
||||||
|
@if (!session('unmatched_rows'))
|
||||||
|
<form action="{{ route('containers.store') }}" method="POST" enctype="multipart/form-data" class="mt-3">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
{{-- Container Name --}}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{{-- Container Number --}}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{{-- Container Date --}}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="cm-form-label">Container Date</label>
|
||||||
|
<input type="date"
|
||||||
|
name="container_date"
|
||||||
|
class="form-control cm-form-control"
|
||||||
|
value="{{ old('container_date') }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Excel File --}}
|
||||||
|
<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>
|
||||||
|
@endsection
|
||||||
291
resources/views/admin/container_show.blade.php
Normal file
291
resources/views/admin/container_show.blade.php
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
@extends('admin.layouts.app')
|
||||||
|
|
||||||
|
@section('page-title', 'Container Details')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<style>
|
||||||
|
.cm-detail-wrapper {
|
||||||
|
padding: 10px 0 20px 0;
|
||||||
|
}
|
||||||
|
.cm-detail-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-detail-header-card .card-body {
|
||||||
|
padding: 14px 18px;
|
||||||
|
display:flex;
|
||||||
|
justify-content:space-between;
|
||||||
|
align-items:center;
|
||||||
|
gap:10px;
|
||||||
|
}
|
||||||
|
.cm-detail-title {
|
||||||
|
margin:0;
|
||||||
|
font-size:20px;
|
||||||
|
font-weight:600;
|
||||||
|
}
|
||||||
|
.cm-detail-sub {
|
||||||
|
font-size:12px;
|
||||||
|
opacity:0.9;
|
||||||
|
}
|
||||||
|
.cm-detail-main-card {
|
||||||
|
border-radius:14px;
|
||||||
|
border:none;
|
||||||
|
box-shadow:0 6px 18px rgba(15,35,52,0.12);
|
||||||
|
overflow:hidden;
|
||||||
|
}
|
||||||
|
.cm-detail-main-card .card-header {
|
||||||
|
background:#ffffff;
|
||||||
|
border-bottom:1px solid #edf0f5;
|
||||||
|
padding:10px 18px;
|
||||||
|
display:flex;
|
||||||
|
justify-content:space-between;
|
||||||
|
align-items:center;
|
||||||
|
gap:10px;
|
||||||
|
}
|
||||||
|
.cm-detail-main-card .card-header h5 {
|
||||||
|
margin:0;
|
||||||
|
font-size:16px;
|
||||||
|
font-weight:600;
|
||||||
|
}
|
||||||
|
.cm-info-label {
|
||||||
|
font-size:12px;
|
||||||
|
color:#6c757d;
|
||||||
|
font-weight:500;
|
||||||
|
}
|
||||||
|
.cm-info-value {
|
||||||
|
font-size:13px;
|
||||||
|
font-weight:500;
|
||||||
|
color:#343a40;
|
||||||
|
}
|
||||||
|
.cm-table-wrapper {
|
||||||
|
position:relative;
|
||||||
|
max-height: 520px;
|
||||||
|
overflow:auto;
|
||||||
|
border-top:1px solid #edf0f5;
|
||||||
|
}
|
||||||
|
.cm-table {
|
||||||
|
font-size:11.5px;
|
||||||
|
min-width: 1100px;
|
||||||
|
}
|
||||||
|
.cm-table thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: #fff7e0;
|
||||||
|
color:#495057;
|
||||||
|
font-weight:600;
|
||||||
|
border-bottom:1px solid #e0d2a4;
|
||||||
|
white-space:nowrap;
|
||||||
|
}
|
||||||
|
.cm-table tbody tr:nth-child(even) {
|
||||||
|
background:#fafbff;
|
||||||
|
}
|
||||||
|
.cm-table tbody tr:hover {
|
||||||
|
background:#e9f3ff;
|
||||||
|
}
|
||||||
|
.cm-table td,
|
||||||
|
.cm-table th {
|
||||||
|
padding:4px 6px;
|
||||||
|
vertical-align:middle;
|
||||||
|
}
|
||||||
|
.cm-table td {
|
||||||
|
white-space:nowrap;
|
||||||
|
}
|
||||||
|
.cm-table-caption {
|
||||||
|
font-size:11px;
|
||||||
|
color:#868e96;
|
||||||
|
padding:6px 18px 0 18px;
|
||||||
|
}
|
||||||
|
.cm-filter-bar {
|
||||||
|
padding:8px 18px 0 18px;
|
||||||
|
display:flex;
|
||||||
|
justify-content:space-between;
|
||||||
|
align-items:center;
|
||||||
|
gap:10px;
|
||||||
|
flex-wrap:wrap;
|
||||||
|
}
|
||||||
|
.cm-filter-input {
|
||||||
|
max-width:240px;
|
||||||
|
font-size:12px;
|
||||||
|
border-radius:20px;
|
||||||
|
padding:6px 10px;
|
||||||
|
}
|
||||||
|
.cm-edit-save-btn {
|
||||||
|
font-size:12px;
|
||||||
|
border-radius:20px;
|
||||||
|
padding:6px 14px;
|
||||||
|
}
|
||||||
|
.cm-cell-input {
|
||||||
|
width: 140px;
|
||||||
|
min-width: 120px;
|
||||||
|
max-width: 220px;
|
||||||
|
font-size:11px;
|
||||||
|
padding:3px 4px;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.cm-detail-header-card .card-body {
|
||||||
|
flex-direction:column;
|
||||||
|
align-items:flex-start;
|
||||||
|
}
|
||||||
|
.cm-table-wrapper {
|
||||||
|
max-height:400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container-fluid cm-detail-wrapper">
|
||||||
|
|
||||||
|
{{-- TOP GRADIENT HEADER --}}
|
||||||
|
<div class="card cm-detail-header-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div>
|
||||||
|
<h4 class="cm-detail-title">
|
||||||
|
Container: {{ $container->container_number }}
|
||||||
|
</h4>
|
||||||
|
<div class="cm-detail-sub">
|
||||||
|
Edit loading list directly – scroll horizontally and vertically like Excel.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('containers.index') }}" class="btn btn-light btn-sm">
|
||||||
|
← Back to list
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- MAIN CARD --}}
|
||||||
|
<div class="card cm-detail-main-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Container Information</h5>
|
||||||
|
|
||||||
|
@if(!$container->rows->isEmpty())
|
||||||
|
{{-- Save button (submits form below) --}}
|
||||||
|
<button type="submit"
|
||||||
|
form="cm-edit-rows-form"
|
||||||
|
class="btn btn-primary cm-edit-save-btn">
|
||||||
|
💾 Save Changes
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body pb-0">
|
||||||
|
{{-- BASIC INFO --}}
|
||||||
|
<div class="row g-3 mb-2">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="cm-info-label">Container</div>
|
||||||
|
<div class="cm-info-value">{{ $container->container_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="cm-info-label">Date</div>
|
||||||
|
<div class="cm-info-value">
|
||||||
|
{{ $container->container_date?->format('d-m-Y') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="cm-info-label">Excel File</div>
|
||||||
|
@if($container->excel_file)
|
||||||
|
<div class="cm-info-value">
|
||||||
|
<a href="{{ \Illuminate\Support\Facades\Storage::url($container->excel_file) }}"
|
||||||
|
target="_blank">
|
||||||
|
Download / View Excel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="cm-info-value text-muted">Not uploaded</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($container->rows->isEmpty())
|
||||||
|
<div class="p-3">
|
||||||
|
<p class="mb-0">No entries found for this container.</p>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
@php
|
||||||
|
// सर्व headings collect
|
||||||
|
$allHeadings = [];
|
||||||
|
foreach ($container->rows as $row) {
|
||||||
|
if (is_array($row->data)) {
|
||||||
|
$allHeadings = array_unique(array_merge($allHeadings, array_keys($row->data)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
{{-- FILTER BAR --}}
|
||||||
|
<div class="cm-filter-bar">
|
||||||
|
<div class="cm-table-caption">
|
||||||
|
Total rows: {{ $container->rows->count() }} • Type to filter rows, edit cells then click "Save Changes".
|
||||||
|
</div>
|
||||||
|
<input type="text" id="cmRowSearch" class="form-control cm-filter-input"
|
||||||
|
placeholder="Quick search in table..." onkeyup="cmFilterRows()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- EDITABLE TABLE FORM --}}
|
||||||
|
<form id="cm-edit-rows-form"
|
||||||
|
action="{{ route('containers.rows.update', $container->id) }}"
|
||||||
|
method="POST">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="cm-table-wrapper mt-1">
|
||||||
|
<table class="table table-bordered table-hover cm-table" id="cmExcelTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
@foreach($allHeadings as $heading)
|
||||||
|
<th>{{ $heading }}</th>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($container->rows as $row)
|
||||||
|
<tr>
|
||||||
|
@foreach($allHeadings as $heading)
|
||||||
|
@php
|
||||||
|
$value = $row->data[$heading] ?? '';
|
||||||
|
@endphp
|
||||||
|
<td>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control form-control-sm cm-cell-input"
|
||||||
|
name="rows[{{ $row->id }}][{{ $heading }}]"
|
||||||
|
value="{{ $value }}">
|
||||||
|
</td>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- SIMPLE FRONT‑END SEARCH --}}
|
||||||
|
<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++) { // skip header
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -698,11 +698,48 @@
|
|||||||
<div class="stats-icon">
|
<div class="stats-icon">
|
||||||
<i class="bi bi-currency-rupee"></i>
|
<i class="bi bi-currency-rupee"></i>
|
||||||
</div>
|
</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 class="stats-label">Total Amount Spent</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- {{-- Total Payable --}}
|
||||||
|
<div class="stats-card amount">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{{-- Total Remaining --}}
|
||||||
|
<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 class="col-md-4 animate-fade-in animation-delay-3">
|
||||||
|
<div class="stats-card marks">
|
||||||
|
<div class="stats-icon">
|
||||||
|
<i class="bi bi-hash"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stats-value">₹{{ number_format($totalPayable, 2) }}</div>
|
||||||
|
<div class="stats-label">Total Payable</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 animate-fade-in animation-delay-3">
|
||||||
|
<div class="stats-card marks">
|
||||||
|
<div class="stats-icon">
|
||||||
|
<i class="bi bi-hash"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stats-value">₹{{ number_format($totalRemaining, 2) }}</div>
|
||||||
|
<div class="stats-label">Remaining Amount</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{{-- Mark Count --}}
|
{{-- Mark Count --}}
|
||||||
<div class="col-md-4 animate-fade-in animation-delay-3">
|
<div class="col-md-4 animate-fade-in animation-delay-3">
|
||||||
<div class="stats-card marks">
|
<div class="stats-card marks">
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 17px 17px 0 0;
|
border-radius: 17px 17px 0 0;
|
||||||
background: #fceeb8ff;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
min-height: 54px;
|
min-height: 54px;
|
||||||
padding: 15px 26px 10px 22px;
|
padding: 15px 26px 10px 22px;
|
||||||
border-bottom: 1.4px solid #e8e2cf;
|
border-bottom: 1.4px solid #e8e2cf;
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
.invoice-management-title {
|
.invoice-management-title {
|
||||||
font-size: 1.32rem;
|
font-size: 1.32rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: #2451af;
|
color: #ffffffff;
|
||||||
letter-spacing: .08em;
|
letter-spacing: .08em;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
|
|
||||||
/* Center all table content */
|
/* Center all table content */
|
||||||
.table thead tr {
|
.table thead tr {
|
||||||
background: #feebbe !important;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table thead th:first-child {
|
.table thead th:first-child {
|
||||||
@@ -237,7 +237,7 @@
|
|||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
border: none;
|
border: none;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #343535;
|
color: #ffffffff;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 20px 15px;
|
padding: 20px 15px;
|
||||||
@@ -258,25 +258,33 @@
|
|||||||
|
|
||||||
/* Soft blue background for ALL table rows */
|
/* Soft blue background for ALL table rows */
|
||||||
.table-striped tbody tr {
|
.table-striped tbody tr {
|
||||||
background: #f0f8ff !important;
|
background: #f0f8ff !important;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.15s ease;
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
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(odd),
|
||||||
.table-striped tbody tr:nth-of-type(even) {
|
.table-striped tbody tr:nth-of-type(even) {
|
||||||
background: #f0f8ff !important;
|
background: #f0f8ff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Center all table cells with proper spacing */
|
|
||||||
.table td {
|
.table td {
|
||||||
padding: 18px 15px;
|
padding: 18px 15px;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -291,7 +299,7 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* First and last cell rounded corners */
|
|
||||||
.table td:first-child {
|
.table td:first-child {
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -587,7 +595,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.date-separator {
|
.date-separator {
|
||||||
color: #64748b;
|
color: #000000ff;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,21 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
<title>Admin Panel</title>
|
<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@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" />
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||||
|
<!-- ✅ CRITICAL: CSRF Token for Echo -->
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
|
<title>@yield('page-title', 'Admin Panel')</title>
|
||||||
|
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
@@ -20,7 +28,6 @@
|
|||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ✨ Sidebar Glass + Animated Highlight Effect */
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
@@ -39,7 +46,6 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar collapsed state */
|
|
||||||
.sidebar.collapsed {
|
.sidebar.collapsed {
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -62,10 +68,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .word {
|
.sidebar .word {
|
||||||
color: #800000; font-size: 13px; line-height: 1.24;
|
color: #800000;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.24;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 🔥 Sidebar Links */
|
|
||||||
.sidebar a {
|
.sidebar a {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -82,7 +89,6 @@
|
|||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Background Animation */
|
|
||||||
.sidebar a::before {
|
.sidebar a::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -107,7 +113,6 @@
|
|||||||
color: #1258e0 !important;
|
color: #1258e0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icon bounce effect */
|
|
||||||
.sidebar a i {
|
.sidebar a i {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
@@ -119,7 +124,6 @@
|
|||||||
color: #1258e0 !important;
|
color: #1258e0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Active link glow effect */
|
|
||||||
.sidebar a.active {
|
.sidebar a.active {
|
||||||
background: linear-gradient(90deg, rgba(80,120,255,0.15), rgba(120,180,255,0.2));
|
background: linear-gradient(90deg, rgba(80,120,255,0.15), rgba(120,180,255,0.2));
|
||||||
color: #1258e0 !important;
|
color: #1258e0 !important;
|
||||||
@@ -134,7 +138,6 @@
|
|||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Logout Button */
|
|
||||||
.sidebar form button {
|
.sidebar form button {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
@@ -148,7 +151,6 @@
|
|||||||
transform: scale(1.03);
|
transform: scale(1.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 🧭 Main Content */
|
|
||||||
.main-content {
|
.main-content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@@ -160,13 +162,11 @@
|
|||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main content when sidebar is collapsed */
|
|
||||||
.main-content.expanded {
|
.main-content.expanded {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header hamburger button */
|
|
||||||
.header-toggle {
|
.header-toggle {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 10px 18px !important;
|
padding: 10px 18px !important;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 48px;
|
height: 65px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
@@ -219,95 +219,136 @@
|
|||||||
font-size: 1.06rem;
|
font-size: 1.06rem;
|
||||||
font-weight: 500;
|
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>
|
</style>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img src="{{ asset('images/kent_logo2.png') }}" alt="Kent Logo">
|
<img src="{{ asset('images/kent_logo2.png') }}" alt="Kent Logo">
|
||||||
<div class="word"><strong>KENT</strong><br /><small>International Pvt. Ltd.</small></div>
|
<div class="word">
|
||||||
|
<strong>KENT</strong><br />
|
||||||
|
<small>International Pvt. Ltd.</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Dashboard (requires order.view) --}}
|
{{-- Dashboard (requires order.view) --}}
|
||||||
@can('order.view')
|
@can('order.view')
|
||||||
<a href="{{ route('admin.dashboard') }}" class="{{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
<a href="{{ route('admin.dashboard') }}" class="{{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
||||||
<i class="bi bi-house"></i> Dashboard
|
<i class="bi bi-house"></i> Dashboard
|
||||||
</a>
|
</a>
|
||||||
@endcan
|
@endcan
|
||||||
|
<!--
|
||||||
{{-- Shipments --}}
|
{{-- Shipments --}}
|
||||||
@can('shipment.view')
|
@can('shipment.view')
|
||||||
<a href="{{ route('admin.shipments') }}" class="{{ request()->routeIs('admin.shipments') ? 'active' : '' }}">
|
<a href="{{ route('admin.shipments') }}" class="{{ request()->routeIs('admin.shipments') ? 'active' : '' }}">
|
||||||
<i class="bi bi-truck"></i> Shipments
|
<i class="bi bi-truck"></i> Shipments
|
||||||
</a>
|
</a>
|
||||||
|
@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
|
@endcan
|
||||||
|
|
||||||
{{-- Invoice --}}
|
{{-- Invoice --}}
|
||||||
@can('invoice.view')
|
@can('invoice.view')
|
||||||
<a href="{{ route('admin.invoices.index') }}" class="{{ request()->routeIs('admin.invoices.index') ? 'active' : '' }}">
|
<a href="{{ route('admin.invoices.index') }}" class="{{ request()->routeIs('admin.invoices.index') ? 'active' : '' }}">
|
||||||
<i class="bi bi-receipt"></i> Invoice
|
<i class="bi bi-receipt"></i> Invoice
|
||||||
</a>
|
</a>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
{{-- Customers --}}
|
{{-- Customers --}}
|
||||||
@can('customer.view')
|
@can('customer.view')
|
||||||
<a href="{{ route('admin.customers.index') }}" class="{{ request()->routeIs('admin.customers.index') ? 'active' : '' }}">
|
<a href="{{ route('admin.customers.index') }}" class="{{ request()->routeIs('admin.customers.index') ? 'active' : '' }}">
|
||||||
<i class="bi bi-people"></i> Customers
|
<i class="bi bi-people"></i> Customers
|
||||||
</a>
|
</a>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
{{-- Reports --}}
|
{{-- Reports --}}
|
||||||
@can('report.view')
|
@can('report.view')
|
||||||
<a href="{{ route('admin.reports') }}" class="{{ request()->routeIs('admin.reports') ? 'active' : '' }}">
|
<a href="{{ route('admin.reports') }}" class="{{ request()->routeIs('admin.reports') ? 'active' : '' }}">
|
||||||
<i class="bi bi-graph-up"></i> Reports
|
<i class="bi bi-graph-up"></i> Reports
|
||||||
</a>
|
</a>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
{{-- Chat Support (NO PERMISSION REQUIRED) --}}
|
{{-- Chat Support --}}
|
||||||
<a href="{{ route('admin.chat_support') }}" class="{{ request()->routeIs('admin.chat_support') ? 'active' : '' }}">
|
@can('chat_support.view')
|
||||||
<i class="bi bi-chat-dots"></i> Chat Support
|
<a href="{{ route('admin.chat_support') }}" class="{{ request()->routeIs('admin.chat_support') ? 'active' : '' }}">
|
||||||
</a>
|
<i class="bi bi-chat-dots"></i> Chat Support
|
||||||
|
</a>
|
||||||
|
@endcan
|
||||||
|
|
||||||
{{-- Orders --}}
|
{{-- Orders --}}
|
||||||
@can('orders.view')
|
@can('orders.view')
|
||||||
<a href="{{ route('admin.orders') }}" class="{{ request()->routeIs('admin.orders') ? 'active' : '' }}">
|
<a href="{{ route('admin.orders') }}" class="{{ request()->routeIs('admin.orders') ? 'active' : '' }}">
|
||||||
<i class="bi bi-bag"></i> Orders
|
<i class="bi bi-bag"></i> Orders
|
||||||
</a>
|
</a>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
{{-- Requests --}}
|
{{-- Requests --}}
|
||||||
@can('request.view')
|
@can('request.view')
|
||||||
<a href="{{ route('admin.requests') }}" class="{{ request()->routeIs('admin.requests') ? 'active' : '' }}">
|
<a href="{{ route('admin.requests') }}" class="{{ request()->routeIs('admin.requests') ? 'active' : '' }}">
|
||||||
<i class="bi bi-envelope"></i> Requests
|
<i class="bi bi-envelope"></i> Requests
|
||||||
</a>
|
</a>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
{{-- Profile Update Requests --}}
|
<!-- {{-- Profile Update Requests --}}
|
||||||
@can('request.update_profile')
|
@can('request.update_profile')
|
||||||
<a href="{{ route('admin.profile.requests') }}">
|
<a href="{{ route('admin.profile.requests') }}">
|
||||||
<i class="bi bi-person-lines-fill"></i> Profile Update Requests
|
<i class="bi bi-person-lines-fill"></i> Profile Update Requests
|
||||||
</a>
|
</a>
|
||||||
@endcan
|
@endcan -->
|
||||||
|
|
||||||
{{-- Staff (NO PERMISSION REQUIRED) --}}
|
{{-- Staff --}}
|
||||||
<a href="{{ route('admin.staff.index') }}" class="{{ request()->routeIs('admin.staff.*') ? 'active' : '' }}">
|
@can('staff.view')
|
||||||
<i class="bi bi-person-badge"></i> Staff
|
<a href="{{ route('admin.staff.index') }}" class="{{ request()->routeIs('admin.staff.*') ? 'active' : '' }}">
|
||||||
</a>
|
<i class="bi bi-person-badge"></i> Staff
|
||||||
|
</a>
|
||||||
|
@endcan
|
||||||
|
|
||||||
{{-- Account Section --}}
|
{{-- Account Section --}}
|
||||||
@can('account.view')
|
@can('account.view')
|
||||||
<a href="{{ route('admin.account') }}" class="{{ request()->routeIs('admin.account') ? 'active' : '' }}">
|
<a href="{{ route('admin.account') }}" class="{{ request()->routeIs('admin.account') ? 'active' : '' }}">
|
||||||
<i class="bi bi-gear"></i> Account
|
<i class="bi bi-gear"></i> Account
|
||||||
</a>
|
</a>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
{{-- Mark List --}}
|
{{-- Mark List --}}
|
||||||
@can('mark_list.view')
|
@can('mark_list.view')
|
||||||
<a href="{{ route('admin.marklist.index') }}" class="{{ request()->routeIs('admin.marklist.index') ? 'active' : '' }}">
|
<a href="{{ route('admin.marklist.index') }}" class="{{ request()->routeIs('admin.marklist.index') ? 'active' : '' }}">
|
||||||
<i class="bi bi-list-check"></i> Mark List
|
<i class="bi bi-list-check"></i> Mark List
|
||||||
</a>
|
</a>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
@@ -325,44 +366,53 @@
|
|||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<a href="#" class="d-flex align-items-center text-decoration-none dropdown-toggle" data-bs-toggle="dropdown">
|
<a href="#" class="d-flex align-items-center text-decoration-none dropdown-toggle" data-bs-toggle="dropdown">
|
||||||
<img src="https://i.pravatar.cc/40" class="rounded-circle me-2" width="35" height="35">
|
<img src="https://i.pravatar.cc/40" class="rounded-circle me-2" width="35" height="35">
|
||||||
<span class="dropdown-user-profile-name">{{ Auth::guard('admin')->user()->name ?? 'User' }}</span>
|
<span class="dropdown-user-profile-name">
|
||||||
|
{{ auth('admin')->user()->name ?? 'User' }}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li><a class="dropdown-item" href="{{ route('admin.profile') }}"><i class="bi bi-person-circle me-2"></i>Profile</a></li>
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ route('admin.profile') }}">
|
||||||
|
<i class="bi bi-person-circle me-2"></i>Profile
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li>
|
<li>
|
||||||
<form method="POST" action="{{ route('admin.logout') }}">
|
<form method="POST" action="{{ route('admin.logout') }}">
|
||||||
@csrf
|
@csrf
|
||||||
<button class="dropdown-item" type="submit"><i class="bi bi-box-arrow-right me-2"></i>Logout</button>
|
<button class="dropdown-item" type="submit">
|
||||||
|
<i class="bi bi-box-arrow-right me-2"></i>Logout
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="content-wrapper">
|
<div class="content-wrapper">
|
||||||
@yield('content')
|
@yield('content')
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<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>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const headerToggle = document.getElementById('headerToggle');
|
const headerToggle = document.getElementById('headerToggle');
|
||||||
const sidebar = document.querySelector('.sidebar');
|
const sidebar = document.querySelector('.sidebar');
|
||||||
const mainContent = document.querySelector('.main-content');
|
const mainContent = document.querySelector('.main-content');
|
||||||
|
|
||||||
// Function to toggle sidebar
|
|
||||||
function toggleSidebar() {
|
function toggleSidebar() {
|
||||||
sidebar.classList.toggle('collapsed');
|
sidebar.classList.toggle('collapsed');
|
||||||
mainContent.classList.toggle('expanded');
|
mainContent.classList.toggle('expanded');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header toggle button click event
|
|
||||||
if (headerToggle) {
|
if (headerToggle) {
|
||||||
headerToggle.addEventListener('click', toggleSidebar);
|
headerToggle.addEventListener('click', toggleSidebar);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -11,60 +11,64 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h3>Orders Report</h3>
|
|
||||||
|
|
||||||
@if(!empty($filters))
|
<h3>Orders Report</h3>
|
||||||
<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
|
|
||||||
|
|
||||||
<table>
|
@if(!empty($filters))
|
||||||
<thead>
|
<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>
|
<tr>
|
||||||
<th>Order ID</th>
|
<td>{{ $order->order_id }}</td>
|
||||||
<th>Shipment ID</th>
|
<td>{{ $shipment?->shipment_id ?? '-' }}</td>
|
||||||
<th>Customer ID</th>
|
<td>{{ $mark?->customer_id ?? '-' }}</td>
|
||||||
<th>Company</th>
|
<td>{{ $mark?->company_name ?? '-' }}</td>
|
||||||
<th>Origin</th>
|
<td>{{ $mark?->origin ?? $order->origin ?? '-' }}</td>
|
||||||
<th>Destination</th>
|
<td>{{ $mark?->destination ?? $order->destination ?? '-' }}</td>
|
||||||
<th>Order Date</th>
|
<td>{{ $order->created_at?->format('d-m-Y') ?? '-' }}</td>
|
||||||
<th>Invoice No</th>
|
<td>{{ $invoice?->invoice_number ?? '-' }}</td>
|
||||||
<th>Invoice Date</th>
|
<td>{{ $invoice?->invoice_date ? \Carbon\Carbon::parse($invoice->invoice_date)->format('d-m-Y') : '-' }}</td>
|
||||||
<th>Amount</th>
|
<td>{{ $invoice?->final_amount !== null ? number_format($invoice->final_amount, 2) : '-' }}</td>
|
||||||
<th>Amount + GST</th>
|
<td>{{ $invoice?->final_amount_with_gst !== null ? number_format($invoice->final_amount_with_gst, 2) : '-' }}</td>
|
||||||
<th>Invoice Status</th>
|
<td>{{ ucfirst($invoice?->status ?? 'Pending') }}</td>
|
||||||
<th>Shipment Status</th>
|
<td>{{ ucfirst(str_replace('_',' ', $shipment?->status ?? 'Pending')) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
@empty
|
||||||
<tbody>
|
<tr>
|
||||||
@forelse($orders as $order)
|
<td colspan="13" style="text-align:center">No orders found</td>
|
||||||
@php
|
</tr>
|
||||||
$mark = $order->markList ?? null;
|
@endforelse
|
||||||
$invoice = $order->invoice ?? null;
|
</tbody>
|
||||||
$shipment = $order->shipments->first() ?? null;
|
</table>
|
||||||
@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>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -13,47 +13,54 @@
|
|||||||
<div class="d-flex justify-content-between align-items-start">
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="fw-bold mb-0">Order Details</h4>
|
<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>
|
<small class="text-muted">Detailed view of this shipment order</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- ADD ITEM --}}
|
{{-- ADD ITEM --}}
|
||||||
@can('order.create')
|
@can('order.create')
|
||||||
<button class="btn btn-add-item" data-bs-toggle="modal" data-bs-target="#addItemModal">
|
@if($status === 'pending')
|
||||||
<i class="fas fa-plus-circle me-2"></i>Add New Item
|
<button class="btn btn-add-item" data-bs-toggle="modal" data-bs-target="#addItemModal">
|
||||||
</button>
|
<i class="fas fa-plus-circle me-2"></i>Add New Item
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
|
|
||||||
<a href="{{ route('admin.dashboard') }}" class="btn-close"></a>
|
<a href="{{ route('admin.dashboard') }}" class="btn-close"></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- {{-- ACTION BUTTONS --}}
|
<!-- {{-- 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 --}} -->
|
{{-- Delete Order --}}
|
||||||
<!-- @if($order->status === 'pending')
|
@if($status === 'pending')
|
||||||
<button class="btn btn-edit-order" onclick="document.getElementById('editOrderForm').style.display='block'">
|
<form action="{{ route('admin.orders.destroy', $order->id) }}"
|
||||||
<i class="fas fa-edit me-2"></i>Edit Order
|
method="POST"
|
||||||
</button>
|
onsubmit="return confirm('Delete this entire order?')">
|
||||||
@else
|
@csrf
|
||||||
<button class="btn btn-edit-order" disabled><i class="fas fa-edit me-2"></i>Edit Order</button>
|
@method('DELETE')
|
||||||
@endif -->
|
<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>
|
<hr>
|
||||||
|
|
||||||
@@ -190,32 +197,33 @@
|
|||||||
<td>{{ $item->ttl_kg }}</td>
|
<td>{{ $item->ttl_kg }}</td>
|
||||||
<td>{{ $item->shop_no }}</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 --}}
|
{{-- DELETE BUTTON --}}
|
||||||
@can('order.edit')
|
@can('order.delete')
|
||||||
<button
|
<form action="{{ route('admin.orders.deleteItem', $item->id) }}"
|
||||||
type="button"
|
method="POST"
|
||||||
class="btn btn-sm btn-edit-item"
|
onsubmit="return confirm('Delete this item?')">
|
||||||
data-bs-toggle="modal"
|
@csrf
|
||||||
data-bs-target="#editItemModal{{ $item->id }}">
|
@method('DELETE')
|
||||||
<i class="fas fa-edit"></i>
|
<button type="submit" class="btn btn-sm btn-delete-item">
|
||||||
</button>
|
<i class="fas fa-trash"></i>
|
||||||
@endcan
|
</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>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
@@ -617,7 +625,7 @@ function fillFormFromDeleted(item) {
|
|||||||
box-shadow: 0 4px 15px 0 rgba(102, 126, 234, 0.3);
|
box-shadow: 0 4px 15px 0 rgba(102, 126, 234, 0.3);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-right: -800px;
|
margin-right: -650px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-add-item:hover {
|
.btn-add-item:hover {
|
||||||
|
|||||||
@@ -4,177 +4,167 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{{ $invoice->invoice_number }}</title>
|
<title>{{ $invoice->invoice_number }}</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', 'Roboto', Arial, sans-serif;
|
font-family: 'Segoe UI', 'Roboto', Arial, sans-serif;
|
||||||
background: #F7FBFC;
|
background: #F7FBFC;
|
||||||
color: #1A222B;
|
color: #1A222B;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
.container {
|
|
||||||
max-width: 850px;
|
|
||||||
margin: 24px auto 0 auto;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 13px;
|
|
||||||
box-shadow: 0 2px 14px rgba(40,105,160,0.08);
|
|
||||||
padding: 35px 32px 18px 32px;
|
|
||||||
}
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
border-bottom: 2px solid #E6EBF0;
|
|
||||||
padding-bottom: 13px;
|
|
||||||
}
|
|
||||||
.logo-company {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.logo {
|
|
||||||
height: 50px;
|
|
||||||
margin-right: 13px;
|
|
||||||
}
|
|
||||||
.company-details {
|
|
||||||
margin-top: 0;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
.company-title {
|
|
||||||
font-size: 21px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
.company-sub {
|
|
||||||
font-size: 16px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.invoice-details {
|
|
||||||
text-align: right;
|
|
||||||
min-width: 220px;
|
|
||||||
}
|
|
||||||
.invoice-title {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 23px;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
.paid-label {
|
|
||||||
margin-top: 8px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
.paid-tag {
|
|
||||||
background: #23BF47;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: bold;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 4px 16px 4px 22px;
|
|
||||||
font-size: 17px;
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.paid-tag:before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 7px;
|
|
||||||
top: 7px;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
.paid-date {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #23BF47;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bill-section {
|
.container {
|
||||||
background: #F3F7FB;
|
max-width: 850px;
|
||||||
border-radius: 11px;
|
margin: 24px auto 0 auto;
|
||||||
padding: 20px 18px 13px 18px;
|
background: #fff;
|
||||||
margin: 28px 0 16px 0;
|
border-radius: 13px;
|
||||||
box-shadow: 0 0px 0px #0000;
|
box-shadow: 0 2px 14px rgba(40,105,160,0.08);
|
||||||
}
|
padding: 35px 32px 18px 32px;
|
||||||
.bill-title {
|
}
|
||||||
font-size: 17px;
|
|
||||||
font-weight: bold;
|
/* ================= HEADER FIX ================= */
|
||||||
color: #23355D;
|
|
||||||
margin-bottom: 4px;
|
.header {
|
||||||
letter-spacing: 0.3px;
|
width: 100%;
|
||||||
}
|
overflow: hidden; /* clears floats */
|
||||||
.bill-details {
|
border-bottom: 2px solid #E6EBF0;
|
||||||
font-size: 15px;
|
padding-bottom: 13px;
|
||||||
line-height: 1.6;
|
}
|
||||||
}
|
|
||||||
|
/* LEFT SIDE */
|
||||||
|
.logo-company {
|
||||||
|
float: left;
|
||||||
|
width: 65%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RIGHT SIDE */
|
||||||
|
.invoice-details {
|
||||||
|
float: right;
|
||||||
|
width: 35%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo */
|
||||||
|
.logo {
|
||||||
|
height: 50px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Company text */
|
||||||
|
.company-details {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-title {
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Invoice */
|
||||||
|
.invoice-title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 23px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Paid / Status */
|
||||||
|
.paid-tag {
|
||||||
|
background: #23BF47;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px 16px 4px 22px;
|
||||||
|
font-size: 17px;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paid-tag:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 7px;
|
||||||
|
top: 7px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paid-date {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #23BF47;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= REST ================= */
|
||||||
|
|
||||||
|
.bill-section {
|
||||||
|
background: #F3F7FB;
|
||||||
|
border-radius: 11px;
|
||||||
|
padding: 20px 18px 13px 18px;
|
||||||
|
margin: 28px 0 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bill-title {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #23355D;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bill-details {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 9px;
|
||||||
|
margin-bottom: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #F6F7F9;
|
||||||
|
padding: 10px 0;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #6781A6;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 7px 0;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:not(:last-child) td {
|
||||||
|
border-bottom: 1px solid #E6EBF0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
border-top: 1.2px solid #E6EBF0;
|
||||||
|
margin-top: 25px;
|
||||||
|
padding-top: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #888;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.gst-row td {
|
||||||
|
color: #23BF47;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-row td {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 17px;
|
||||||
|
border-top: 2px solid #E6EBF0;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin-top: 9px;
|
|
||||||
margin-bottom: 13px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
th {
|
|
||||||
background: #F6F7F9;
|
|
||||||
padding: 10px 0;
|
|
||||||
font-size: 15px;
|
|
||||||
color: #6781A6;
|
|
||||||
font-weight: bold;
|
|
||||||
border: none;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
padding: 7px 0;
|
|
||||||
color: #222;
|
|
||||||
font-size: 15px;
|
|
||||||
border: none;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
tbody tr:not(:last-child) td {
|
|
||||||
border-bottom: 1px solid #E6EBF0;
|
|
||||||
}
|
|
||||||
tbody tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
.totals-row td {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #23355D;
|
|
||||||
}
|
|
||||||
.gst-row td {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #23BF47;
|
|
||||||
}
|
|
||||||
.total-row td {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 17px;
|
|
||||||
color: #222;
|
|
||||||
}
|
|
||||||
.payment-info {
|
|
||||||
margin-top: 24px;
|
|
||||||
margin-bottom: 9px;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
.ref-number {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #6781A6;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
.footer {
|
|
||||||
border-top: 1.2px solid #E6EBF0;
|
|
||||||
margin-top: 25px;
|
|
||||||
padding-top: 12px;
|
|
||||||
font-size: 16px;
|
|
||||||
color: #888;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.footer strong {
|
|
||||||
color: #222;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -241,18 +231,47 @@
|
|||||||
<td>{{ number_format($item->ttl_amount, 0) }}</td>
|
<td>{{ number_format($item->ttl_amount, 0) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
{{-- SUBTOTAL --}}
|
||||||
<tr class="totals-row">
|
<tr class="totals-row">
|
||||||
<td colspan="3" style="text-align:right;">Subtotal:</td>
|
<td colspan="3" style="text-align:right;">total</td>
|
||||||
<td>{{ number_format($invoice->subtotal, 0) }}</td>
|
<td style="text-align:right;">
|
||||||
</tr>
|
₹ {{ number_format($invoice->final_amount, 2) }}
|
||||||
<tr class="gst-row">
|
</td>
|
||||||
<td colspan="3" style="text-align:right;">GST ({{ $invoice->gst_percent }}%):</td>
|
|
||||||
<td>{{ number_format($invoice->gst_amount, 0) }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
{{-- TAX --}}
|
||||||
|
@if($invoice->tax_type === 'gst' && $invoice->gst_amount > 0)
|
||||||
|
|
||||||
|
<tr class="gst-row">
|
||||||
|
<td colspan="3" style="text-align:right;">
|
||||||
|
GST ({{ $invoice->gst_percent }}%)
|
||||||
|
</td>
|
||||||
|
<td style="text-align:right;">
|
||||||
|
₹ {{ number_format($invoice->gst_amount, 2) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
@elseif($invoice->tax_type === 'igst' && $invoice->gst_amount > 0)
|
||||||
|
|
||||||
|
<tr class="gst-row">
|
||||||
|
<td colspan="3" style="text-align:right;">
|
||||||
|
IGST ({{ $invoice->gst_percent }}%)
|
||||||
|
</td>
|
||||||
|
<td style="text-align:right;">
|
||||||
|
₹ {{ number_format($invoice->gst_amount, 2) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- TOTAL --}}
|
||||||
<tr class="total-row">
|
<tr class="total-row">
|
||||||
<td colspan="3" style="text-align:right;">Total:</td>
|
<td colspan="3" style="text-align:right;">Total Amount</td>
|
||||||
<td>{{ number_format($invoice->final_amount_with_gst, 0) }}</td>
|
<td style="text-align:right;">
|
||||||
|
₹ {{ number_format($invoice->final_amount_with_gst, 2) }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<!-- Payment Info & Reference -->
|
<!-- Payment Info & Reference -->
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,107 +5,207 @@
|
|||||||
@section('content')
|
@section('content')
|
||||||
<div class="container-fluid px-0">
|
<div class="container-fluid px-0">
|
||||||
|
|
||||||
@php
|
@php
|
||||||
$perPage = 5;
|
$perPage = 5;
|
||||||
$currentPage = request()->get('page', 1);
|
$currentPage = request()->get('page', 1);
|
||||||
$currentPage = max(1, (int)$currentPage);
|
$currentPage = max(1, (int)$currentPage);
|
||||||
$total = $requests->count();
|
$total = $requests->count();
|
||||||
$totalPages = ceil($total / $perPage);
|
$currentItems = $requests->slice(($currentPage - 1) * $perPage, $perPage);
|
||||||
$currentItems = $requests->slice(($currentPage - 1) * $perPage, $perPage);
|
@endphp
|
||||||
@endphp
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.old-value { color: #6b7280; font-weight: 600; }
|
/* ===== Card Wrapper ===== */
|
||||||
.new-value { color: #111827; font-weight: 700; }
|
.request-card {
|
||||||
.changed { background: #fef3c7; padding: 6px; border-radius: 6px; }
|
background: #ffffff;
|
||||||
.box { padding: 10px 14px; border-radius: 8px; background: #f8fafc; margin-bottom: 10px; }
|
border-radius: 14px;
|
||||||
.diff-label { font-size: 13px; font-weight: 700; }
|
padding: 18px;
|
||||||
.actions { display: flex; gap: 10px; }
|
margin-bottom: 18px;
|
||||||
</style>
|
box-shadow: 0 6px 20px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
<h4 class="fw-bold my-3">Profile Update Requests ({{ $total }})</h4>
|
/* ===== Header Row ===== */
|
||||||
|
.request-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 60px 1.5fr 1fr 1.2fr 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
<div class="card mb-4 shadow-sm">
|
.request-header strong {
|
||||||
<div class="card-body pb-1">
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
<div class="table-responsive custom-table-wrapper">
|
/* ===== Badges ===== */
|
||||||
<table class="table align-middle mb-0 custom-table">
|
.badge {
|
||||||
<thead>
|
padding: 6px 14px;
|
||||||
<tr>
|
font-size: 12px;
|
||||||
<th>#</th>
|
border-radius: 999px;
|
||||||
<th>User</th>
|
font-weight: 700;
|
||||||
<th>Requested Changes</th>
|
}
|
||||||
<th>Status</th>
|
|
||||||
<th>Requested At</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
.badge-pending {
|
||||||
@foreach($currentItems as $index => $req)
|
background: #fff7ed;
|
||||||
@php
|
color: #c2410c;
|
||||||
$user = $req->user;
|
border: 1px solid #fed7aa;
|
||||||
// FIX: Convert string to array
|
}
|
||||||
$newData = is_array($req->data) ? $req->data : json_decode($req->data, true);
|
|
||||||
@endphp
|
|
||||||
|
|
||||||
<tr>
|
.badge-approved {
|
||||||
<td><strong>{{ ($currentPage - 1) * $perPage + $index + 1 }}</strong></td>
|
background: #ecfdf5;
|
||||||
|
color: #047857;
|
||||||
|
border: 1px solid #a7f3d0;
|
||||||
|
}
|
||||||
|
|
||||||
<td>
|
.badge-rejected {
|
||||||
<strong>{{ $user->customer_name }}</strong><br>
|
background: #fef2f2;
|
||||||
<small>{{ $user->email }}</small><br>
|
color: #b91c1c;
|
||||||
<small>ID: {{ $user->customer_id }}</small>
|
border: 1px solid #fecaca;
|
||||||
</td>
|
}
|
||||||
|
|
||||||
<td>
|
/* ===== Action Buttons ===== */
|
||||||
@foreach($newData as $key => $newValue)
|
.actions {
|
||||||
@php
|
display: flex;
|
||||||
$oldValue = $user->$key ?? '—';
|
gap: 10px;
|
||||||
$changed = $oldValue != $newValue;
|
}
|
||||||
@endphp
|
|
||||||
|
|
||||||
<div class="box {{ $changed ? 'changed' : '' }}">
|
.actions .btn {
|
||||||
<span class="diff-label">{{ ucfirst(str_replace('_',' ', $key)) }}:</span><br>
|
padding: 6px 14px;
|
||||||
<span class="old-value">Old: {{ $oldValue }}</span><br>
|
font-size: 13px;
|
||||||
<span class="new-value">New: {{ $newValue ?? '—' }}</span>
|
border-radius: 999px;
|
||||||
</div>
|
font-weight: 600;
|
||||||
@endforeach
|
}
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
/* ===== Detail Grid ===== */
|
||||||
@if($req->status == 'pending')
|
.detail-grid {
|
||||||
<span class="badge badge-pending">Pending</span>
|
display: grid;
|
||||||
@elseif($req->status == 'approved')
|
grid-template-columns: repeat(3, 1fr);
|
||||||
<span class="badge badge-approved">Approved</span>
|
gap: 14px;
|
||||||
@else
|
margin-top: 12px;
|
||||||
<span class="badge badge-rejected">Rejected</span>
|
}
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>{{ $req->created_at->format('d M Y, h:i A') }}</td>
|
/* ===== Detail Box ===== */
|
||||||
|
.detail-box {
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
<td class="actions">
|
.detail-box.changed {
|
||||||
@if($req->status == 'pending')
|
background: linear-gradient(145deg, #fff7ed, #ffedd5);
|
||||||
<a href="{{ route('admin.profile.approve', $req->id) }}" class="btn btn-success btn-sm">
|
border-left: 4px solid #f59e0b;
|
||||||
<i class="bi bi-check-circle"></i> Approve
|
}
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="{{ route('admin.profile.reject', $req->id) }}" class="btn btn-danger btn-sm">
|
.detail-label {
|
||||||
<i class="bi bi-x-circle"></i> Reject
|
font-size: 12px;
|
||||||
</a>
|
font-weight: 700;
|
||||||
@else
|
color: #334155;
|
||||||
<span class="text-muted">Completed</span>
|
}
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
.old {
|
||||||
@endforeach
|
font-size: 12px;
|
||||||
</tbody>
|
color: #64748b;
|
||||||
</table>
|
}
|
||||||
</div>
|
|
||||||
|
|
||||||
|
.new {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #020617;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Responsive ===== */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.request-header {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<h4 class="fw-bold my-3">Profile Update Requests ({{ $total }})</h4>
|
||||||
|
|
||||||
|
@foreach($currentItems as $index => $req)
|
||||||
|
@php
|
||||||
|
$user = $req->user;
|
||||||
|
$newData = is_array($req->data) ? $req->data : json_decode($req->data, true);
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="request-card">
|
||||||
|
|
||||||
|
<!-- HEADER -->
|
||||||
|
<div class="request-header">
|
||||||
|
<strong>#{{ ($currentPage - 1) * $perPage + $index + 1 }}</strong>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<strong>{{ $user->customer_name }}</strong><br>
|
||||||
|
<small>{{ $user->email }}</small><br>
|
||||||
|
<small>ID: {{ $user->customer_id }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
@if($req->status == 'pending')
|
||||||
|
<span class="badge badge-pending">Pending</span>
|
||||||
|
@elseif($req->status == 'approved')
|
||||||
|
<span class="badge badge-approved">Approved</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-rejected">Rejected</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>{{ $req->created_at->format('d M Y, h:i A') }}</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
@if($req->status == 'pending')
|
||||||
|
<a href="{{ route('admin.profile.approve', $req->id) }}" class="btn btn-success btn-sm">
|
||||||
|
✔ Approve
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.profile.reject', $req->id) }}" class="btn btn-danger btn-sm">
|
||||||
|
✖ Reject
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<span class="text-muted">Completed</span>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- DETAILS ROW 1 -->
|
||||||
|
<div class="detail-grid">
|
||||||
|
@foreach(['customer_name','company_name','email'] as $field)
|
||||||
|
@php
|
||||||
|
$old = $user->$field ?? '—';
|
||||||
|
$new = $newData[$field] ?? $old;
|
||||||
|
@endphp
|
||||||
|
<div class="detail-box {{ $old != $new ? 'changed' : '' }}">
|
||||||
|
<div class="detail-label">{{ ucfirst(str_replace('_',' ', $field)) }}</div>
|
||||||
|
<div class="old">Old: {{ $old }}</div>
|
||||||
|
<div class="new">New: {{ $new }}</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DETAILS ROW 2 -->
|
||||||
|
<div class="detail-grid">
|
||||||
|
@foreach(['mobile_no','address','pincode'] as $field)
|
||||||
|
|
||||||
|
@php
|
||||||
|
$old = $user->$field ?? '—';
|
||||||
|
$new = $newData[$field] ?? $old;
|
||||||
|
@endphp
|
||||||
|
<div class="detail-box {{ $old != $new ? 'changed' : '' }}">
|
||||||
|
<div class="detail-label">{{ ucfirst(str_replace('_',' ', $field)) }}</div>
|
||||||
|
<div class="old">Old: {{ $old }}</div>
|
||||||
|
<div class="new">New: {{ $new }}</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -912,10 +912,6 @@
|
|||||||
</label>
|
</label>
|
||||||
<select class="filter-control" id="companyFilter">
|
<select class="filter-control" id="companyFilter">
|
||||||
<option value="" selected>All Companies</option>
|
<option value="" selected>All Companies</option>
|
||||||
<option value="ABC Corporation">ABC Corporation</option>
|
|
||||||
<option value="XYZ Enterprises">XYZ Enterprises</option>
|
|
||||||
<option value="Global Traders">Global Traders</option>
|
|
||||||
<option value="Tech Solutions Ltd">Tech Solutions Ltd</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
@@ -1101,6 +1097,7 @@
|
|||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Initialize statistics and pagination
|
// Initialize statistics and pagination
|
||||||
|
populateCompanyFilter(allReports);
|
||||||
updateStatistics();
|
updateStatistics();
|
||||||
renderTable();
|
renderTable();
|
||||||
updatePaginationControls();
|
updatePaginationControls();
|
||||||
@@ -1403,5 +1400,33 @@
|
|||||||
// Initial filter application
|
// Initial filter application
|
||||||
filterTable();
|
filterTable();
|
||||||
});
|
});
|
||||||
|
function populateCompanyFilter(reports) {
|
||||||
|
const companySelect = document.getElementById('companyFilter');
|
||||||
|
if (!companySelect) return;
|
||||||
|
|
||||||
|
// Collect unique company names
|
||||||
|
const companies = new Set();
|
||||||
|
|
||||||
|
reports.forEach(r => {
|
||||||
|
if (r.company_name && r.company_name.trim() !== '') {
|
||||||
|
companies.add(r.company_name.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort alphabetically
|
||||||
|
const sortedCompanies = Array.from(companies).sort();
|
||||||
|
|
||||||
|
// Remove existing options except "All Companies"
|
||||||
|
companySelect.innerHTML = '<option value="">All Companies</option>';
|
||||||
|
|
||||||
|
// Append options
|
||||||
|
sortedCompanies.forEach(company => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = company;
|
||||||
|
option.textContent = company;
|
||||||
|
companySelect.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
.custom-table tbody tr:hover { background-color: #fffbea; transform: scale(1.01); box-shadow: 0 2px 6px rgba(0,0,0,0.05); }
|
.custom-table tbody tr:hover { background-color: #fffbea; transform: scale(1.01); box-shadow: 0 2px 6px rgba(0,0,0,0.05); }
|
||||||
.priority-badge {
|
.priority-badge {
|
||||||
display: inline-flex; align-items: center; font-size: 13.5px; padding: 6px 16px; border-radius: 12px; font-weight: 600;
|
display: inline-flex; align-items: center; font-size: 13.5px; padding: 6px 16px; border-radius: 12px; font-weight: 600;
|
||||||
box-shadow: 0 1px 2px 0 rgba(130,130,130,0.15); width: 90px; min-height: 28px; justify-content: center;
|
box-shadow: 0 1px 2px 0 rgba(230, 206, 206, 0.15); width: 90px; min-height: 28px; justify-content: center;
|
||||||
color: #fff; margin: 2px 0; transition: transform 0.2s ease-in-out;
|
color: #fff; margin: 2px 0; transition: transform 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
.priority-badge:hover { transform: scale(1.08); }
|
.priority-badge:hover { transform: scale(1.08); }
|
||||||
@@ -30,8 +30,8 @@
|
|||||||
.priority-medium { background: linear-gradient(135deg, #ffe390, #f5b041); }
|
.priority-medium { background: linear-gradient(135deg, #ffe390, #f5b041); }
|
||||||
.priority-low { background: linear-gradient(135deg, #b8f0c2, #1d8660); }
|
.priority-low { background: linear-gradient(135deg, #b8f0c2, #1d8660); }
|
||||||
.custom-table thead th {
|
.custom-table thead th {
|
||||||
text-align: center; font-weight: 700; color: #000; padding: 14px; font-size: 17px; letter-spacing: 0.5px;
|
text-align: center; font-weight: 700; color: #ffffffff; padding: 14px; font-size: 17px; letter-spacing: 0.5px;
|
||||||
border-bottom: 2px solid #bfbfbf; background-color: #fde4b3;
|
border-bottom: 2px solid #bfbfbf; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);;
|
||||||
}
|
}
|
||||||
.custom-table thead tr:first-child th:first-child { border-top-left-radius: 12px; }
|
.custom-table thead tr:first-child th:first-child { border-top-left-radius: 12px; }
|
||||||
.custom-table thead tr:first-child th:last-child { border-top-right-radius: 12px; }
|
.custom-table thead tr:first-child th:last-child { border-top-right-radius: 12px; }
|
||||||
@@ -307,13 +307,58 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* ==============================================
|
||||||
|
PROFILE UPDATE REQUEST BUTTON BADGE FIX
|
||||||
|
============================================== */
|
||||||
|
|
||||||
|
/* Ensure button is positioning context */
|
||||||
|
a.btn.btn-primary.position-relative {
|
||||||
|
position: relative;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix badge inside Profile Update Requests button */
|
||||||
|
a.btn.btn-primary.position-relative .badge {
|
||||||
|
width: 30px !important;
|
||||||
|
height: 30px !important;
|
||||||
|
min-width: 30px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
line-height: 30px !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;
|
||||||
|
|
||||||
|
}
|
||||||
|
.custom-table th,
|
||||||
|
.custom-table td {
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Counts -->
|
<!-- Counts -->
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2 mt-3">
|
<div class="d-flex justify-content-between align-items-center mb-2 mt-3">
|
||||||
<h4 class="fw-bold mb-0">User Requests (Total: {{ $total }})</h4>
|
<h4 class="fw-bold mb-0">User Requests (Total: {{ $total }})</h4>
|
||||||
|
|
||||||
</div>
|
@can('request.update_profile')
|
||||||
|
<a href="{{ route('admin.profile.requests') }}" class="btn btn-primary position-relative">
|
||||||
|
<i class="bi bi-person-lines-fill me-1"></i>
|
||||||
|
Profile Update Requests
|
||||||
|
|
||||||
|
@if($pendingProfileUpdates > 0)
|
||||||
|
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
|
||||||
|
{{ $pendingProfileUpdates }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</a>
|
||||||
|
@endcan
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Search + Table -->
|
<!-- Search + Table -->
|
||||||
<div class="card mb-4 shadow-sm">
|
<div class="card mb-4 shadow-sm">
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user